Redis (Upstash)
Fast key-value data: counters, rate limits, sessions, caches.
What it is
Upstash Redis is installed from the Vercel Marketplace, like Neon. Redis keeps data in memory, so reads and writes take about a millisecond, and its commands are atomic: INCR can't lose a count however many requests arrive at once. @upstash/redis talks to it over HTTPS, so functions need no open connections, and @upstash/ratelimit builds rate limits on top.
When to use it
- Counters and leaderboards
- Rate limiting APIs and forms
- Short-lived data: sessions, caches, job state
1. An atomic counter (INCR)
Reload the page: every view adds one.
Counting…
2. Rate limiting
A sliding window of 5 requests per 30 seconds per visitor (by IP address). Click quickly: the sixth click is refused until the window moves on.
The same library protects the lab's real writes: Blob uploads and guestbook notes share a budget of 5 per minute per visitor.
Show code · src/lib/redis.ts
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
import { headers } from "next/headers";
// Upstash Redis, through the Vercel Marketplace. Like Neon, it's spoken to
// over HTTPS, so there are no connections to keep open. Redis.fromEnv() reads
// KV_REST_API_URL and KV_REST_API_TOKEN, which the integration set.
export const redisConfigured = () =>
Boolean((process.env.UPSTASH_REDIS_REST_URL || process.env.KV_REST_API_URL) && (process.env.UPSTASH_REDIS_REST_TOKEN || process.env.KV_REST_API_TOKEN));
// Created on first use, so a build without the variables doesn't fail.
let client: Redis | undefined;
export const redis = () => (client ??= Redis.fromEnv());
const limiters = new Map<string, Ratelimit>();
const limiter = (name: string, tokens: number, window: `${number} s` | `${number} m`) => {
if (!limiters.has(name)) {
limiters.set(name, new Ratelimit({ redis: redis(), limiter: Ratelimit.slidingWindow(tokens, window), prefix: `lab:rl:${name}` }));
}
return limiters.get(name)!;
};
// Every write in the lab (Blob uploads, guestbook notes) shares one budget per
// visitor; the Redis page has its own, smaller one to play with.
export const writeLimit = () => limiter("writes", 5, "1 m");
export const demoLimit = () => limiter("demo", 5, "30 s");
// CSP reports come from browsers automatically; capped so nobody can flood the log.
export const reportLimit = () => limiter("reports", 20, "1 m");
// Who to count: the visitor's IP. Vercel sets x-forwarded-for itself (any
// value the client sent is replaced), so its first entry can be trusted there.
export async function clientKey() {
const h = await headers();
return h.get("x-forwarded-for")?.split(",")[0]?.trim() || h.get("x-real-ip") || "local";
}
export type LimitResult = { success: boolean; limit: number; remaining: number; reset: number };
// Checks a limit, failing open if Redis isn't configured: the demos keep
// working without it, just unthrottled.
export async function checkLimit(get: () => Ratelimit): Promise<LimitResult | null> {
if (!redisConfigured()) return null;
const { success, limit, remaining, reset } = await get().limit(await clientKey());
return { success, limit, remaining, reset };
}
export const retryIn = (reset: number) => Math.max(1, Math.ceil((reset - Date.now()) / 1000));
Show code · src/app/redis/page.tsx
import { connection } from "next/server";
import { Suspense } from "react";
import { CodePeek } from "@/components/CodePeek";
import { Demo, FeaturePage } from "@/components/FeaturePage";
import { redis, redisConfigured } from "@/lib/redis";
import { timed } from "@/lib/timed";
import { LimitButton } from "./LimitButton";
// INCR is atomic: two requests at the same moment can't both read 41 and
// write 42. One round trip both counts this view and returns the total.
async function Views() {
await connection();
if (!redisConfigured()) return <p className="error">Redis isn't connected for this environment.</p>;
const { value: views, ms } = await timed(() => redis().incr("lab:views:redis"));
return (
<div className="cells">
<div className="cell"><span className="small muted">Views of this page</span><strong className="big-number" style={{ textAlign: "left" }}>{views}</strong></div>
<div className="cell"><span className="small muted">INCR round trip</span><strong className="stamp">{ms.toFixed(1)} ms</strong></div>
</div>
);
}
export default function RedisPage() {
return (
<FeaturePage
title="Redis (Upstash)"
tagline="Fast key-value data: counters, rate limits, sessions, caches."
docs="https://vercel.com/marketplace/upstash"
whatItIs={<p>
<strong>Upstash Redis</strong> is installed from the Vercel Marketplace, like Neon. Redis keeps data in memory, so reads and
writes take about a millisecond, and its commands are atomic: <code>INCR</code> can't lose a count however many requests
arrive at once. <code>@upstash/redis</code> talks to it over HTTPS, so functions need no open connections, and{" "}
<code>@upstash/ratelimit</code> builds rate limits on top.
</p>}
whenToUse={[
"Counters and leaderboards",
"Rate limiting APIs and forms",
"Short-lived data: sessions, caches, job state",
]}
>
<Demo title="1. An atomic counter (INCR)">
<p>Reload the page: every view adds one.</p>
<Suspense fallback={<p className="muted">Counting…</p>}>
<Views />
</Suspense>
</Demo>
<Demo title="2. Rate limiting">
<p>
A sliding window of <strong>5 requests per 30 seconds</strong> per visitor (by IP address). Click quickly: the sixth click
is refused until the window moves on.
</p>
<LimitButton />
<p className="small muted">
The same library protects the lab's real writes: Blob uploads and guestbook notes share a budget of 5 per minute per
visitor.
</p>
</Demo>
<CodePeek file="lib/redis.ts" />
<CodePeek file="app/redis/page.tsx" />
</FeaturePage>
);
}