Redis Cache Lab
Set/get with TTL and exercise distributed rate limits on Upstash Redis from the Marketplace.
Run this experiment yourself
Demos are not embedded on this site. Deploy a standalone copy on Vercel or run the experiment app locally.
Local development
cd apps/experiments/redis-cache-lab pnpm install pnpm dev
Then open http://localhost:3010.
This is an experimental demo. Use it as a starting point for your own projects.
The Redis Cache Lab exercises Upstash Redis, provisioned from the Vercel Marketplace, over its REST API. You can set a string value with a TTL, read it back with its remaining TTL, delete it, and hit a fixed-window rate limiter that uses atomic INCR and EXPIRE. Because state lives in Redis rather than in a function instance, every serverless invocation sees the same keys.
Features
- Set with TTL. Store a string (up to 2000 characters) for 1 to 3600 seconds. The default is 60.
- Get with remaining TTL. Read the value and the key's TTL in parallel.
- Delete. Remove a key from the lab namespace.
- Distributed rate limiting.
POST /api/rate-limitimplements a fixed window withINCR+EXPIREand returns429once the limit is exceeded. The lab's Hit rate limit button uses bucketredis-lab, limit5, window15seconds. - Namespaced keys. Cache keys are stored as
experiments:redis-lab:<key>, rate-limit buckets asexperiments:ratelimit:<bucket>. - Honest missing-config state. Without credentials the routes return
503with asetuphint and the UI shows a warning.
API Reference
If Redis credentials are not configured, every method below returns 503 with { error: "Redis is not configured", setup }.
GET /api/redis/cache
Prop
Type
POST /api/redis/cache
Sets a string value with an expiry (SET ... EX).
Prop
Type
DELETE /api/redis/cache
Prop
Type
POST /api/rate-limit
Fixed-window counter used by the lab (and shared with the Rate Limiting Simulator).
Prop
Type
Implementation Details
Create one cached client
lib/vercel/redis.ts builds an Upstash client from REST credentials and caches it for the life of the module. It accepts the Marketplace variables and the legacy Vercel KV REST variables, and returns null when either the URL or token is missing.
const url =
process.env.UPSTASH_REDIS_REST_URL?.trim() ||
process.env.KV_REST_API_URL?.trim();
const token =
process.env.UPSTASH_REDIS_REST_TOKEN?.trim() ||
process.env.KV_REST_API_TOKEN?.trim();
if (url && token) {
cached = new Redis({ url, token });
return cached;
}Fail with a setup message
Every handler starts with the same guard so a missing store never looks like a server crash.
function unavailable() {
return NextResponse.json(
{
error: 'Redis is not configured',
setup:
'Add Upstash Redis via the Vercel Marketplace (one-click Deploy includes it) so UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN are injected.',
},
{ status: 503 },
);
}Validate, then namespace and set with TTL
const KEY_PREFIX = 'experiments:redis-lab:';
const MAX_TTL = 60 * 60;
const setSchema = z.object({
key: z.string().min(1).max(64).regex(/^[a-zA-Z0-9:_-]+$/),
value: z.string().min(1).max(2000),
ttlSeconds: z.number().int().min(1).max(MAX_TTL).default(60),
});
const fullKey = `${KEY_PREFIX}${parsed.data.key}`;
await redis.set(fullKey, parsed.data.value, { ex: parsed.data.ttlSeconds });Read value and TTL together
const [value, ttl] = await Promise.all([redis.get<string>(fullKey), redis.ttl(fullKey)]);Count requests per window
The rate limiter increments a bucket key and sets the expiry only when the counter is created (count === 1), which defines a fixed window.
const key = `experiments:ratelimit:${bucket}`;
const count = await redis.incr(key);
if (count === 1) {
await redis.expire(key, windowSeconds);
}
const ttl = await redis.ttl(key);
const allowed = count <= limit;The response status is 200 when allowed and 429 otherwise.
Clamp on the client too
apps/experiments/redis-cache-lab/logic.ts exports clampTtl(), which the demo uses to keep the UI inside the server's 1–3600 range.
export function clampTtl(seconds: number): number {
return Math.min(3600, Math.max(1, Math.floor(seconds)));
}Use Cases
- Response and computation caching with automatic expiry.
- Distributed rate limiting that works across serverless instances, unlike in-memory counters.
- Ephemeral session or one-time state such as short-lived tokens and webhook captures.
- Shared counters and idempotency keys across concurrent invocations.
Limitations
- Fixed-window only. The limiter can allow a burst at a window boundary (up to roughly twice the limit across two adjacent windows). It is not a sliding window or token bucket.
- Non-atomic expiry.
INCRandEXPIREare separate calls. If the function stops between them the counter can be left without an expiry. The demo accepts this; use a Lua script or a library such as@upstash/ratelimitfor stricter guarantees. - Strings only. The cache route stores and returns strings, with a 2000-character cap and a 1-hour maximum TTL.
- No authentication. Anyone who can reach the deployment can set, read, and delete keys under
experiments:redis-lab:. - Shared rate-limit buckets.
/api/rate-limitbuckets are global perbucketname, not per user or IP. - Not persistent storage. Treat Redis here as a cache; values expire by design.
Use in your project
Cache an expensive result with a TTL. Install with pnpm add @upstash/redis and provide the REST URL and token.
// lib/cache.ts
import { Redis } from '@upstash/redis';
const redis = Redis.fromEnv();
export async function cached<T>(key: string, ttlSeconds: number, load: () => Promise<T>) {
const hit = await redis.get<T>(key);
if (hit !== null) return hit;
const value = await load();
await redis.set(key, value, { ex: ttlSeconds });
return value;
}Redis.fromEnv() reads UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN. This repository builds the client explicitly in lib/vercel/redis.ts so it can also fall back to KV_REST_API_URL and KV_REST_API_TOKEN.
Deployment
Use the one-click Deploy button. It clones the repository and provisions Upstash Redis from the Vercel Marketplace, which injects the REST credentials.
Local Development
pnpm install
pnpm devPull credentials from a linked project, or set them yourself:
vercel env pull .env.local
# or
echo 'UPSTASH_REDIS_REST_URL=https://<your-db>.upstash.io' >> .env.local
echo 'UPSTASH_REDIS_REST_TOKEN=<token>' >> .env.localTry the API (default port 3000):
# Set a value for 60 seconds
curl -X POST http://localhost:3000/api/redis/cache \
-H "content-type: application/json" \
-d '{"key":"greeting","value":"hello from upstash","ttlSeconds":60}'
# Read it back (includes ttlSeconds)
curl "http://localhost:3000/api/redis/cache?key=greeting"
# Delete it
curl -X DELETE "http://localhost:3000/api/redis/cache?key=greeting"
# Hit the rate limiter (5 requests per 15 seconds, then 429)
for i in 1 2 3 4 5 6; do
curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:3000/api/rate-limit \
-H "content-type: application/json" \
-d '{"bucket":"redis-lab","limit":5,"windowSeconds":15}'
doneExpected response when Redis is missing:
{
"error": "Redis is not configured",
"setup": "Add Upstash Redis via the Vercel Marketplace (one-click Deploy includes it) so UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN are injected."
}Configuration
From .env.example:
| Variable | Required | Purpose |
|---|---|---|
UPSTASH_REDIS_REST_URL | Yes, for live demos | Upstash Redis REST endpoint. |
UPSTASH_REDIS_REST_TOKEN | Yes, for live demos | Upstash Redis REST token. |
KV_REST_API_URL | Legacy alternative | Accepted in place of UPSTASH_REDIS_REST_URL. |
KV_REST_API_TOKEN | Legacy alternative | Accepted in place of UPSTASH_REDIS_REST_TOKEN. |
Both a URL and a token are required; if either is missing the client is null and routes return 503.
Vercel / Next.js Features Used
- Vercel Marketplace storage integration (Upstash Redis) provisioned through the Deploy Button
storesparameter (REDIS_STOREinlib/vercel/deploy.ts). - Upstash Redis REST client (
@upstash/redis):setwithex,get,ttl,del,incr, andexpire. - Route Handlers with
GET,POST, andDELETEexports. - Zod for request validation.