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
Official docs ↗

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&apos;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&apos;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&apos;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>
  );
}