6.8 KiB
name, description, license, compatibility, metadata
| name | description | license | compatibility | metadata | ||
|---|---|---|---|---|---|---|
| upstash-redis | Use Redis over HTTP from serverless and edge runtimes with @upstash/redis, and add rate limiting with @upstash/ratelimit. Use when the user mentions Upstash Redis, needs Redis from a Next.js route handler or middleware, Vercel, Cloudflare Workers, Deno, or Bun without TCP connection pooling, or wants cache-aside with TTLs, a session store, counters, or a 429 rate limiter using fixed window, sliding window, or token bucket. DO NOT use for self-hosted or TCP Redis clients (ioredis, node-redis), Redis Cluster administration, or vector similarity search. | MIT | @upstash/redis 1.x, @upstash/ratelimit 2.x, Node.js 18+ or any runtime with global fetch |
|
Upstash Redis Skill
This skill covers the three things serverless apps most often need Redis for: caching, sessions, and rate limiting. The client talks to Redis over HTTP, so it works where a long-lived TCP connection does not (edge middleware, short lived functions). Follow the steps in order; each ends with a checkpoint.
Requirements and limitations
- An Upstash Redis database (hosted service; usage-based pricing with a free tier). Credentials are a REST URL and token from the database page.
- Environment variables
UPSTASH_REDIS_REST_URLandUPSTASH_REDIS_REST_TOKEN. - Every command is an HTTP request. Batch with
pipeline()orMGET/MSETwhen you issue many commands per request; avoidKEYS *in production. - Values are serialized automatically (objects, arrays, numbers round-trip).
Do not
JSON.stringifybeforesetorparseIntafterget.
Step 1 — Install and create one client per module
npm install @upstash/redis @upstash/ratelimit
// lib/redis.ts
import { Redis } from "@upstash/redis";
// Reads UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN
export const redis = Redis.fromEnv();
Create the client at module scope, not inside the request handler, so ephemeral caches and pipelines can be reused across invocations.
Checkpoint:
await redis.ping()returns"PONG".
Step 2 — Cache-aside with TTL
import { redis } from "@/lib/redis";
type User = { id: string; name: string; plan: "free" | "pro" };
export async function getUser(userId: string): Promise<User | null> {
const key = `user:${userId}`;
const cached = await redis.get<User>(key);
if (cached) return cached;
const user = await db.users.findById(userId); // your data source
if (user) await redis.set(key, user, { ex: 3600 }); // 1 hour TTL
return user;
}
export async function updateUser(userId: string, patch: Partial<User>) {
const user = await db.users.update(userId, patch);
await redis.set(`user:${userId}`, user, { ex: 3600 }); // write-through
return user;
}
export async function deleteUser(userId: string) {
await db.users.delete(userId);
await redis.del(`user:${userId}`); // invalidate
}
Always set a TTL on cache entries; namespace keys (user:123, session:abc).
Checkpoint: second call to
getUserreturns without hitting the database andawait redis.ttl("user:123")is positive.
Step 3 — Sessions with sliding expiration
import { redis } from "@/lib/redis";
const SESSION_TTL = 60 * 60 * 24; // 24 hours
export async function createSession(userId: string, data: Record<string, unknown>) {
const sessionId = crypto.randomUUID();
await redis.set(`session:${sessionId}`, { userId, ...data, createdAt: Date.now() }, { ex: SESSION_TTL });
return sessionId;
}
export async function getSession<T = Record<string, unknown>>(sessionId: string) {
const session = await redis.get<T>(`session:${sessionId}`);
if (session) await redis.expire(`session:${sessionId}`, SESSION_TTL); // slide
return session;
}
export async function destroySession(sessionId: string) {
await redis.del(`session:${sessionId}`);
}
Store the session id in an HttpOnly; Secure; SameSite cookie; never put the
Redis token in client code.
Checkpoint:
getSessionaftercreateSessionreturns the object withuserId; afterdestroySessionit returnsnull.
Step 4 — Rate limiting a route handler
// app/api/search/route.ts (Next.js App Router; same pattern for any fetch handler)
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, "10 s"), // 10 requests per 10 seconds
prefix: "ratelimit:search", // isolate keys per limiter
});
export async function POST(request: Request) {
const ip = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "anonymous";
const { success, limit, remaining, reset } = await ratelimit.limit(ip);
if (!success) {
return new Response("Too Many Requests", {
status: 429,
headers: {
"X-RateLimit-Limit": String(limit),
"X-RateLimit-Remaining": String(remaining),
"Retry-After": String(Math.max(0, Math.ceil((reset - Date.now()) / 1000))),
},
});
}
// handle the request
return Response.json({ ok: true });
}
- Identifier: use the user id or API key when authenticated; fall back to IP.
- Algorithms:
Ratelimit.fixedWindow(n, "1 m")(cheapest),slidingWindow(smooth boundaries, default choice),tokenBucket(refill, "10 s", max)(allows bursts). Windows acceptms,s,m,h,d. - Tiers: create one
Ratelimitper tier with differentprefixvalues. - Edge middleware / Cloudflare Workers with
analytics: true: the result has apendingpromise; pass it tocontext.waitUntil(pending)so background work finishes before the runtime exits. resetis a Unix timestamp in milliseconds.
Checkpoint: the 11th request within 10 seconds returns 429 with a
Retry-Afterheader; after the window it succeeds again.
Common pitfalls
- Creating clients inside handlers: the limiter's in-memory
ephemeralCacheonly helps when the instance outlives the request. - Manual JSON:
redis.set("k", JSON.stringify(v))thenredis.getreturns an already-parsed object; double parsing throws. - No TTL on cache keys: memory grows until eviction; always pass
{ ex }. - Trusting
x-forwarded-forblindly: take the first hop, or use the platform's IP helper, when behind a proxy. - Forgetting
pendingon edge runtimes with analytics or multi-region limiters.
When NOT to use this skill
- Long-running servers with a TCP Redis connection already in place: keep ioredis/node-redis.
- Vector search or RAG: use a vector database skill instead.
- Sub-millisecond, in-process caching: use an in-memory LRU.