Skip to main content
This guide demonstrates how to use Upstash Redis in Next.js applications, covering both App Router and Pages Router patterns, Server Components, API Routes, and Server Actions.

Why Upstash Redis for Next.js?

  • Edge-ready: Works in Edge Runtime and Serverless Functions
  • Server Components: Native support for React Server Components
  • Fast: HTTP-based for quick cold starts on Vercel
  • Simple: No connection pooling required
  • Flexible: Use in API routes, Server Actions, or Server Components

Prerequisites

1

Create Next.js App

npx create-next-app@latest my-app
cd my-app
2

Install Upstash Redis

npm install @upstash/redis
3

Create Upstash Redis Database

Create a database on Upstash Console
4

Add Environment Variables

Create .env.local file:
UPSTASH_REDIS_REST_URL=your_url_here
UPSTASH_REDIS_REST_TOKEN=your_token_here

App Router (Next.js 13+)

Server Component Example

Server Components can directly access Redis without API routes.
import { Redis } from "@upstash/redis";

const redis = Redis.fromEnv();

export default async function Home() {
  const count = await redis.incr("counter");
  
  return (
    <div className="flex h-screen w-screen items-center justify-center">
      <h1 className="text-4xl font-bold">Counter: {count}</h1>
    </div>
  )
}

API Route Example

import { Redis } from "@upstash/redis";
import { NextResponse } from "next/server";

const redis = Redis.fromEnv();

export async function GET() {
  const count = await redis.incr("counter");
  return NextResponse.json({ count });
}

export const dynamic = 'force-dynamic'

Server Actions

Server Actions provide a simple way to mutate data from Client Components.
"use server"

import { Redis } from "@upstash/redis";
import { revalidatePath } from "next/cache";

const redis = Redis.fromEnv();

export async function incrementCounter() {
  const count = await redis.incr("counter");
  revalidatePath("/");
  return count;
}

export async function resetCounter() {
  await redis.set("counter", 0);
  revalidatePath("/");
  return 0;
}

export async function addTodo(text: string) {
  const id = Date.now().toString();
  await redis.hset(`todo:${id}`, {
    id,
    text,
    completed: false,
    createdAt: new Date().toISOString(),
  });
  revalidatePath("/todos");
  return id;
}

Pages Router

API Routes

import type { NextApiRequest, NextApiResponse } from "next";
import { Redis } from "@upstash/redis";

const redis = Redis.fromEnv();

type Data = {
  count: number;
};

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>
) {
  const count = await redis.incr("counter");
  res.status(200).json({ count });
}

Server-Side Rendering (SSR)

pages/index.tsx
import { Redis } from "@upstash/redis";
import type { GetServerSideProps } from "next";

const redis = Redis.fromEnv();

interface Props {
  count: number;
}

export default function Home({ count }: Props) {
  return (
    <div className="flex h-screen items-center justify-center">
      <h1 className="text-4xl font-bold">Counter: {count}</h1>
    </div>
  );
}

export const getServerSideProps: GetServerSideProps<Props> = async () => {
  const count = await redis.get<number>("counter") || 0;
  
  return {
    props: {
      count,
    },
  };
};

Advanced Examples

Caching with Revalidation

app/posts/[id]/page.tsx
import { Redis } from "@upstash/redis";
import { notFound } from "next/navigation";

const redis = Redis.fromEnv();

interface Post {
  id: string;
  title: string;
  content: string;
  author: string;
}

async function getPost(id: string): Promise<Post | null> {
  const cacheKey = `post:${id}`;
  
  // Try cache first
  const cached = await redis.get<Post>(cacheKey);
  if (cached) {
    return cached;
  }
  
  // Fetch from database (simulated)
  const post: Post = {
    id,
    title: "Example Post",
    content: "This is an example post",
    author: "John Doe",
  };
  
  // Cache for 5 minutes
  await redis.set(cacheKey, post, { ex: 300 });
  
  return post;
}

export default async function PostPage({
  params,
}: {
  params: { id: string };
}) {
  const post = await getPost(params.id);
  
  if (!post) {
    notFound();
  }
  
  return (
    <article className="max-w-2xl mx-auto p-8">
      <h1 className="text-3xl font-bold mb-4">{post.title}</h1>
      <p className="text-gray-600 mb-8">By {post.author}</p>
      <div className="prose">{post.content}</div>
    </article>
  );
}

// Revalidate every 60 seconds
export const revalidate = 60;

Rate Limiting Middleware

middleware.ts
import { Redis } from "@upstash/redis";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});

const RATE_LIMIT = 10; // requests per minute
const WINDOW = 60; // seconds

export async function middleware(request: NextRequest) {
  // Get client IP
  const ip = request.ip || "unknown";
  const key = `rate_limit:${ip}`;

  // Increment counter
  const requests = await redis.incr(key);

  // Set expiration on first request
  if (requests === 1) {
    await redis.expire(key, WINDOW);
  }

  // Check if limit exceeded
  if (requests > RATE_LIMIT) {
    return new NextResponse(
      JSON.stringify({ error: "Rate limit exceeded" }),
      {
        status: 429,
        headers: {
          "Content-Type": "application/json",
          "Retry-After": WINDOW.toString(),
        },
      }
    );
  }

  // Add rate limit headers
  const response = NextResponse.next();
  response.headers.set("X-RateLimit-Limit", RATE_LIMIT.toString());
  response.headers.set("X-RateLimit-Remaining", (RATE_LIMIT - requests).toString());

  return response;
}

export const config = {
  matcher: "/api/:path*",
};

Session Management

lib/session.ts
import { Redis } from "@upstash/redis";
import { cookies } from "next/headers";

const redis = Redis.fromEnv();

interface Session {
  userId: string;
  email: string;
  createdAt: number;
}

export async function getSession(): Promise<Session | null> {
  const sessionId = cookies().get("sessionId")?.value;
  
  if (!sessionId) {
    return null;
  }
  
  const session = await redis.get<Session>(`session:${sessionId}`);
  return session;
}

export async function createSession(userId: string, email: string): Promise<string> {
  const sessionId = crypto.randomUUID();
  const session: Session = {
    userId,
    email,
    createdAt: Date.now(),
  };
  
  // Store session for 7 days
  await redis.set(`session:${sessionId}`, session, { ex: 60 * 60 * 24 * 7 });
  
  cookies().set("sessionId", sessionId, {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "lax",
    maxAge: 60 * 60 * 24 * 7, // 7 days
  });
  
  return sessionId;
}

export async function deleteSession(): Promise<void> {
  const sessionId = cookies().get("sessionId")?.value;
  
  if (sessionId) {
    await redis.del(`session:${sessionId}`);
    cookies().delete("sessionId");
  }
}
app/api/auth/login/route.ts
import { createSession } from "@/lib/session";
import { NextResponse } from "next/server";

export async function POST(request: Request) {
  const { email, password } = await request.json();
  
  // Validate credentials (implement your own logic)
  const userId = "user123"; // Retrieved from your auth system
  
  await createSession(userId, email);
  
  return NextResponse.json({ success: true });
}

Real-time View Counter

app/blog/[slug]/page.tsx
import { Redis } from "@upstash/redis";
import ViewCounter from "./ViewCounter";

const redis = Redis.fromEnv();

interface Props {
  params: { slug: string };
}

export default async function BlogPost({ params }: Props) {
  const viewKey = `views:${params.slug}`;
  
  // Increment view count
  const views = await redis.incr(viewKey);
  
  return (
    <article>
      <h1>Blog Post</h1>
      <ViewCounter views={views} />
      {/* Post content */}
    </article>
  );
}
app/blog/[slug]/ViewCounter.tsx
"use client"

export default function ViewCounter({ views }: { views: number }) {
  return (
    <div className="text-sm text-gray-600">
      {views.toLocaleString()} views
    </div>
  );
}

Best Practices

1. Initialize Client Once

Create a shared Redis instance:
lib/redis.ts
import { Redis } from "@upstash/redis";

export const redis = Redis.fromEnv();
Use it throughout your app:
import { redis } from "@/lib/redis";

const data = await redis.get("key");

2. Type Safety

Use TypeScript generics for type-safe operations:
interface User {
  id: string;
  name: string;
  email: string;
}

const user = await redis.get<User>("user:123");
// user is typed as User | null

3. Error Handling

import { redis } from "@/lib/redis";
import { NextResponse } from "next/server";

export async function GET() {
  try {
    const data = await redis.get("key");
    return NextResponse.json({ data });
  } catch (error) {
    console.error("Redis error:", error);
    return NextResponse.json(
      { error: "Failed to fetch data" },
      { status: 500 }
    );
  }
}

4. Caching Strategy

Implement cache-aside pattern:
async function getData(id: string) {
  const cacheKey = `data:${id}`;
  
  // Try cache
  const cached = await redis.get(cacheKey);
  if (cached) return cached;
  
  // Fetch from source
  const data = await fetchFromDatabase(id);
  
  // Store in cache
  await redis.set(cacheKey, data, { ex: 300 });
  
  return data;
}

Deployment

Vercel

1

Add Environment Variables

In your Vercel project settings, add:
  • UPSTASH_REDIS_REST_URL
  • UPSTASH_REDIS_REST_TOKEN
2

Deploy

vercel deploy

Other Platforms

Upstash Redis works on any platform that supports Next.js:
  • Netlify
  • AWS Amplify
  • Railway
  • Render
Just add the environment variables to your platform’s settings.

Next Steps

AWS Lambda

Deploy with AWS Lambda

Cloudflare Workers

Use Redis at the edge

Basic Usage

Learn more Redis operations

GitHub Examples

More Next.js examples

Build docs developers (and LLMs) love