Skill Upstash Redis
Ce skill couvre les trois cas d'usage pour lesquels les apps serverless ont le plus souvent besoin de Redis : le caching, les sessions et le rate limiting. Le client communique avec Redis via HTTP, ce qui fonctionne là où une connexion TCP longue durée ne fonctionne pas (edge middleware, functions éphémères). Suivez les étapes dans l'ordre ; chacune se termine par un checkpoint.
Requirements et limitations
- Une base de données Upstash Redis (service hébergé ; tarification à l'usage avec un tier gratuit). Les credentials sont une URL REST et un token depuis la page de la base.
- Variables d'environnement
UPSTASH_REDIS_REST_URLetUPSTASH_REDIS_REST_TOKEN. - Chaque commande est une requête HTTP. Regroupez avec
pipeline()ouMGET/MSETquand vous émettez beaucoup de commandes par requête ; évitezKEYS *en production. - Les valeurs sont sérialisées automatiquement (objets, tableaux, nombres conservent leurs types). Ne faites pas de
JSON.stringifyavantsetou deparseIntaprèsget.
Step 1 — Installer et créer un client par module
npm install @upstash/redis @upstash/ratelimit
// lib/redis.ts
import { Redis } from "@upstash/redis";
// Lit UPSTASH_REDIS_REST_URL et UPSTASH_REDIS_REST_TOKEN
export const redis = Redis.fromEnv();
Créez le client à la portée du module, pas à l'intérieur du request handler, pour que les caches éphémères et les pipelines puissent être réutilisés entre les invocations.
Checkpoint :
await redis.ping()retourne"PONG".
Step 2 — Cache-aside avec 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); // votre source de données
if (user) await redis.set(key, user, { ex: 3600 }); // TTL 1 heure
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}`); // invalider
}
Définissez toujours un TTL sur les entrées du cache ; utilisez des namespaces de clés (user:123, session:abc).
Checkpoint : le deuxième appel à
getUserretourne sans accéder à la base etawait redis.ttl("user:123")est positif.
Step 3 — Sessions avec expiration glissante
import { redis } from "@/lib/redis";
const SESSION_TTL = 60 * 60 * 24; // 24 heures
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); // glisser
return session;
}
export async function destroySession(sessionId: string) {
await redis.del(`session:${sessionId}`);
}
Stockez l'id de session dans un cookie HttpOnly; Secure; SameSite ; ne mettez jamais le token Redis dans le code client.
Checkpoint :
getSessionaprèscreateSessionretourne l'objet avecuserId; aprèsdestroySessionil retournenull.
Step 4 — Rate limiting un route handler
// app/api/search/route.ts (Next.js App Router ; même pattern pour n'importe quel 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 requêtes par 10 secondes
prefix: "ratelimit:search", // isoler les clés par 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))),
},
});
}
// traiter la requête
return Response.json({ ok: true });
}
- Identifiant : utilisez l'id utilisateur ou la clé API quand authentifié ; sinon, l'IP.
- Algorithmes :
Ratelimit.fixedWindow(n, "1 m")(moins cher),slidingWindow(frontières lisses, choix par défaut),tokenBucket(refill, "10 s", max)(permet les pics). Les fenêtres acceptentms,s,m,h,d. - Tiers : créez un
Ratelimitpar tier avec différentes valeursprefix. - Edge middleware / Cloudflare Workers avec
analytics: true: le résultat a une promessepending; passez-la àcontext.waitUntil(pending)pour que le travail de fond se termine avant la sortie du runtime. resetest un timestamp Unix en millisecondes.
Checkpoint : la 11e requête dans 10 secondes retourne 429 avec un en-tête
Retry-After; après la fenêtre, elle réussit à nouveau.
Pièges courants
- Créer les clients à l'intérieur des handlers : l'
ephemeralCacheen mémoire du limiter n'aide que si l'instance survit à la requête. - JSON manuel :
redis.set("k", JSON.stringify(v))puisredis.getretourne déjà un objet parsé ; le double parsing lève une erreur. - Pas de TTL sur les clés du cache : la mémoire grandit jusqu'à l'éviction ; passez toujours
{ ex }. - Faire confiance aveuglément à
x-forwarded-for: prenez le premier hop, ou utilisez le helper IP de la plateforme, quand derrière un proxy. - Oublier
pendingsur les edge runtimes avec analytics ou limiters multi-région.
Quand NE PAS utiliser ce skill
- Serveurs long-running avec une connexion TCP Redis déjà en place : gardez ioredis/node-redis.
- Vector search ou RAG : utilisez un skill vector database à la place.
- Caching in-process, sub-milliseconde : utilisez un LRU en mémoire.