Why Edge Caching and Rate Limiting Matter
When you expose an API to the world, performance and security go hand in hand. In serverless environments, every request can trigger a cold start or a database round-trip, both of which eat into response times. Without rate limiting, a single bad actor can exhaust your resources and drive up costs.
Upstash Redis is a serverless Redis service accessible via HTTP REST API. Unlike traditional Redis that requires persistent TCP connections, Upstash works seamlessly in edge environments like Cloudflare Workers where TCP isn't available. That makes it the perfect companion for building fast, resilient APIs at the edge.
Prerequisites and Setup
Before diving in, you should be comfortable with the following:
- Basic TypeScript syntax
- Cloudflare Workers fundamentals (check out Building AI-Powered Edge Apps with Antigravity and Cloudflare Workers if you need a refresher)
- REST API concepts
What You'll Need
- Antigravity (latest version)
- Node.js 18 or later
- Upstash account (free tier available at upstash.com)
- Cloudflare account (the free Workers tier is enough)
Creating Your Upstash Redis Database
Head over to the Upstash Console and create a new Redis database. Pick a region close to your target users. Once it's created, grab these two values from the REST API section:
# From the Upstash Console REST API section
UPSTASH_REDIS_REST_URL=https://your-database.upstash.io
UPSTASH_REDIS_REST_TOKEN=AXxxxxxxxxxxxxxxxxxxxxxxxxxxxxProject Setup
Open Antigravity's terminal and scaffold a new Cloudflare Workers project using the Hono framework. Antigravity's agent can generate the boilerplate for you in seconds.
# Create the project
npm create hono@latest my-edge-api -- --template cloudflare-workers
cd my-edge-api
# Install the Upstash Redis client and rate limiter
npm install @upstash/redis @upstash/ratelimitThen configure your environment variables in wrangler.toml:
# wrangler.toml
name = "my-edge-api"
compatibility_date = "2026-03-01"
[vars]
ENVIRONMENT = "production"
# Secrets should be set via wrangler CLI
# wrangler secret put UPSTASH_REDIS_REST_URL
# wrangler secret put UPSTASH_REDIS_REST_TOKENImplementing Edge Caching
Using Upstash Redis as a cache layer dramatically reduces database hits and speeds up responses. Here's a reusable caching middleware:
// src/cache.ts
import { Redis } from "@upstash/redis/cloudflare";
import type { Context, Next } from "hono";
// Initialize the Upstash Redis client
function getRedis(c: Context): Redis {
return new Redis({
url: c.env.UPSTASH_REDIS_REST_URL,
token: c.env.UPSTASH_REDIS_REST_TOKEN,
});
}
// Cache middleware
export function cacheMiddleware(ttlSeconds: number = 300) {
return async (c: Context, next: Next) => {
const redis = getRedis(c);
const cacheKey = `cache:${c.req.url}`;
// Try to get from cache
const cached = await redis.get<string>(cacheKey);
if (cached) {
// Cache hit — return the cached response immediately
return c.json(JSON.parse(cached), 200, {
"X-Cache": "HIT",
"X-Cache-TTL": String(ttlSeconds),
});
}
// Cache miss — pass the request through to the origin
await next();
// Store successful responses in cache
if (c.res.status === 200) {
const body = await c.res.clone().text();
await redis.set(cacheKey, body, { ex: ttlSeconds });
c.res.headers.set("X-Cache", "MISS");
}
};
}
// Manually invalidate cache entries matching a pattern
export async function invalidateCache(
redis: Redis,
pattern: string
): Promise<number> {
const keys = await redis.keys(`cache:${pattern}`);
if (keys.length === 0) return 0;
// Use pipeline to batch delete (minimizes network round-trips)
const pipeline = redis.pipeline();
keys.forEach((key) => pipeline.del(key));
await pipeline.exec();
return keys.length;
}The X-Cache header makes it easy to tell whether a response came from cache or from the origin — super helpful for debugging.
Implementing Rate Limiting
With the @upstash/ratelimit package, you can set up sliding window rate limiting in just a few lines:
// src/ratelimit.ts
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis/cloudflare";
import type { Context, Next } from "hono";
// Rate limiting middleware
export function rateLimitMiddleware(
maxRequests: number = 60,
windowSeconds: number = 60
) {
return async (c: Context, next: Next) => {
const redis = new Redis({
url: c.env.UPSTASH_REDIS_REST_URL,
token: c.env.UPSTASH_REDIS_REST_TOKEN,
});
const ratelimit = new Ratelimit({
redis,
// Sliding window: maxRequests allowed per windowSeconds
limiter: Ratelimit.slidingWindow(maxRequests, `${windowSeconds} s`),
analytics: true, // View analytics in the Upstash Console
});
// Identify the client by API key or IP address
const identifier =
c.req.header("X-API-Key") ||
c.req.header("CF-Connecting-IP") ||
"anonymous";
const { success, limit, remaining, reset } =
await ratelimit.limit(identifier);
// Always include rate limit headers
c.res.headers.set("X-RateLimit-Limit", String(limit));
c.res.headers.set("X-RateLimit-Remaining", String(remaining));
c.res.headers.set("X-RateLimit-Reset", String(reset));
if (!success) {
// Rate limit exceeded — return 429 Too Many Requests
return c.json(
{
error: "Too Many Requests",
message: `Rate limit exceeded. Try again after ${new Date(reset).toISOString()}`,
retryAfter: Math.ceil((reset - Date.now()) / 1000),
},
429
);
}
await next();
};
}The sliding window algorithm prevents bursts at window boundaries, giving you fairer rate limiting compared to fixed windows.
Wiring It All Together
Now let's integrate both middlewares into a Hono application:
// src/index.ts
import { Hono } from "hono";
import { cors } from "hono/cors";
import { cacheMiddleware, invalidateCache } from "./cache";
import { rateLimitMiddleware } from "./ratelimit";
import { Redis } from "@upstash/redis/cloudflare";
type Bindings = {
UPSTASH_REDIS_REST_URL: string;
UPSTASH_REDIS_REST_TOKEN: string;
};
const app = new Hono<{ Bindings: Bindings }>();
// Global middleware
app.use("*", cors());
app.use("/api/*", rateLimitMiddleware(100, 60)); // 100 requests per 60 seconds
// Cached endpoint (5-minute TTL)
app.get("/api/articles", cacheMiddleware(300), async (c) => {
// Your actual data fetching logic (DB, external API, etc.)
const articles = await fetchArticlesFromDB();
return c.json({ articles, timestamp: new Date().toISOString() });
});
// Uncached endpoint (for real-time data)
app.get("/api/stats", async (c) => {
const redis = new Redis({
url: c.env.UPSTASH_REDIS_REST_URL,
token: c.env.UPSTASH_REDIS_REST_TOKEN,
});
// Use Redis as a real-time counter
const pageViews = await redis.incr("stats:pageviews");
return c.json({ pageViews });
});
// Cache purge endpoint (admin only)
app.post("/api/cache/purge", async (c) => {
const redis = new Redis({
url: c.env.UPSTASH_REDIS_REST_URL,
token: c.env.UPSTASH_REDIS_REST_TOKEN,
});
const deleted = await invalidateCache(redis, "*articles*");
return c.json({ purged: deleted });
});
// Placeholder data fetching (replace with real DB calls in production)
async function fetchArticlesFromDB() {
return [
{ id: 1, title: "Getting Started with Edge Computing" },
{ id: 2, title: "Redis Patterns for Serverless" },
];
}
export default app;Getting the Most Out of Antigravity's Agent
Antigravity's agent mode excels at generating and refactoring infrastructure code like this. Here are some prompt ideas to try:
# Ask for cache strategy advice
"Suggest optimal cache TTLs for these endpoints:
- /api/articles → updates a few times per day
- /api/user/profile → updates on user action
- /api/stats → needs real-time accuracy"
# Design tiered rate limits
"Design rate limiting for these tiers:
- Free users: 60 req/min
- Pro users: 300 req/min
- Identify by API key"
The agent also handles wrangler.toml config and TypeScript type definitions in one go, so you won't miss any configuration steps. For more on building serverless APIs with Antigravity, see Antigravity × Hono: Building Serverless APIs on Workers.
Deploying and Testing
Deploy with Wrangler and verify everything works:
# Local development
npx wrangler dev
# Set secrets
npx wrangler secret put UPSTASH_REDIS_REST_URL
npx wrangler secret put UPSTASH_REDIS_REST_TOKEN
# Production deploy
npx wrangler deployAfter deploying, test the cache and rate limiting behavior:
# First request: cache MISS
curl -i https://my-edge-api.your-subdomain.workers.dev/api/articles
# → X-Cache: MISS
# Second request: cache HIT (much faster)
curl -i https://my-edge-api.your-subdomain.workers.dev/api/articles
# → X-Cache: HIT
# Rate limit test (fire 105 requests)
for i in $(seq 1 105); do
echo "Request $i: $(curl -s -o /dev/null -w '%{http_code}' https://my-edge-api.your-subdomain.workers.dev/api/articles)"
done
# → Requests 101+ return 429Common Errors and How to Fix Them
Upstash Connection Errors
If UPSTASH_REDIS_REST_URL or UPSTASH_REDIS_REST_TOKEN aren't set correctly, you'll see something like:
Error: Unauthorized - Invalid token
Run wrangler secret list to confirm your secrets are registered. For local development, put your environment variables in a .dev.vars file.
Stale Cache Data
If your cache TTL is too long, users will see outdated data after updates. Either hit the /api/cache/purge endpoint when content changes, or adopt a write-through cache pattern that updates the cache on every write.
Rate Limits Too Strict or Too Loose
Enable analytics: true in your Ratelimit config and check the Upstash Console dashboard to see actual traffic patterns. Adjust maxRequests and windowSeconds based on real data rather than guesswork.
Wrapping Up
By combining Antigravity with Upstash Redis, you can build fast, secure APIs on Cloudflare Workers with minimal effort.
Here's what we covered:
- Upstash Redis uses HTTP REST, making it compatible with edge environments where TCP connections aren't available
- Cache middleware reduces database load and dramatically improves response times
- Rate limiting middleware with sliding windows provides fair, burst-resistant access control
- Antigravity's agent accelerates infrastructure code generation and keeps configurations consistent
As a next step, check out Designing Secure API Backend Patterns with Antigravity to add authentication and authorization to your edge API.