← All posts
·9 min read

Claude Code with Upstash: Redis and Rate Limiting

Claude CodeUpstashRedisRate Limiting
Claude Code with Upstash Redis and Rate Limiting

Why Redis without Upstash-specific rules produces connection leaks

Upstash is a serverless data platform built for edge and serverless runtimes. Its Redis product exposes a REST API instead of a TCP connection, which means it works in Cloudflare Workers, Vercel Edge Functions, and any runtime without Node.js TCP support. Its QStash product is a durable HTTP message queue that survives function timeouts and retries delivery automatically.

The problem is that Claude Code's Redis knowledge comes from ioredis and redis (the npm package), both of which use TCP connections. In a long-running server, a single connection or pool is created at startup and reused. In a serverless function, this pattern creates a new TCP connection on every cold invocation, exhausts the connection limit within seconds of any real traffic, and generates cryptic ECONNREFUSED or ETIMEDOUT errors that look like network failures.

Upstash Redis uses HTTP. There is no connection to manage. The @upstash/redis client wraps the REST API in a Promise-based interface that looks identical to ioredis for basic operations but never opens a socket. Claude does not know this and generates ioredis clients by default.

The second common mistake involves rate limiting. Claude can implement a sliding window rate limiter using Redis sorted sets, but it generates this logic from scratch: a Lua script, pipeline commands, and manual expiry handling. Upstash ships @upstash/ratelimit which implements sliding window, fixed window, and token bucket algorithms as a single function call. The from-scratch implementation has subtle bugs in clock skew handling and pipeline atomicity that the library has already solved.

The CLAUDE.md for an Upstash project should declare the REST client, the rate limiting library, and the QStash patterns so Claude generates the correct abstraction layer from the start.

For background job orchestration on top of QStash, the Claude Code with Inngest guide covers a comparable durable execution model. For the broader Cloudflare edge deployment context where Upstash is commonly used, see Claude Code with Cloudflare Workers.

The Upstash CLAUDE.md template

# Upstash rules

## Stack
- @upstash/redis ^1.x (REST-based Redis client)
- @upstash/ratelimit ^2.x (rate limiting library)
- @upstash/qstash ^2.x (message queue, if used)
- UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN in .env.local
- QSTASH_URL, QSTASH_TOKEN, QSTASH_CURRENT_SIGNING_KEY, QSTASH_NEXT_SIGNING_KEY (if QStash)

## Redis client (MANDATORY)
ALWAYS use @upstash/redis, NEVER ioredis or the 'redis' npm package for serverless contexts:

import { Redis } from '@upstash/redis';
export const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});

- Singleton at module level is fine for serverless (no TCP connections to leak)
- For edge runtimes (Cloudflare Workers, Vercel Edge): same client, same import
- NEVER use process.env.UPSTASH_REDIS_URL (this is the TCP URL, not REST)

## Rate limiting (MANDATORY pattern)
Use @upstash/ratelimit, NEVER implement rate limiting from scratch:

import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '10 s'),
  analytics: true,
  prefix: 'ratelimit',
});

// In route handler:
const { success, limit, remaining, reset } = await ratelimit.limit(identifier);
if (!success) {
  return new Response('Too Many Requests', {
    status: 429,
    headers: {
      'X-RateLimit-Limit': limit.toString(),
      'X-RateLimit-Remaining': remaining.toString(),
      'X-RateLimit-Reset': new Date(reset).toISOString(),
    },
  });
}

## Rate limiter algorithms
- slidingWindow: smooth limiting, no burst at window boundaries (default choice)
- fixedWindow: simpler, allows burst at window reset (use for background jobs)
- tokenBucket: allows controlled burst, good for AI API endpoints
- ALWAYS set analytics: true for monitoring in Upstash console
- ALWAYS set a prefix to namespace rate limit keys by feature

## QStash patterns (if used)
- Publish with @upstash/qstash Client class, NOT raw fetch to QSTASH_URL
- ALWAYS verify webhook signatures with Receiver before processing
- ALWAYS return 200 quickly (QStash retries on non-2xx responses)
- Set content-type: application/json on publish calls
- NEVER process messages in the publish step (publish = fire and forget)

## QStash publish pattern
import { Client } from '@upstash/qstash';
const qstash = new Client({ token: process.env.QSTASH_TOKEN! });
await qstash.publishJSON({
  url: `${process.env.NEXT_PUBLIC_APP_URL}/api/process-job`,
  body: { userId, action },
  retries: 3,
});

## QStash receiver pattern
import { Receiver } from '@upstash/qstash';
const receiver = new Receiver({
  currentSigningKey: process.env.QSTASH_CURRENT_SIGNING_KEY!,
  nextSigningKey: process.env.QSTASH_NEXT_SIGNING_KEY!,
});

## Hard rules
- NEVER use ioredis or redis (tcp) package in serverless/edge contexts
- NEVER skip signature verification on QStash webhook endpoints
- NEVER use UPSTASH_REDIS_URL (tcp): use UPSTASH_REDIS_REST_URL (http)
- NEVER build custom rate limiting when @upstash/ratelimit exists
- ALWAYS handle the { success, remaining, reset } return from ratelimit.limit()
- ALWAYS namespace Redis keys by feature prefix to avoid collisions

Rate limiting patterns for AI API routes

The most common Upstash use case in 2026 is rate limiting AI endpoints. An endpoint that calls the Anthropic or OpenAI API is expensive per request. Without a rate limit, a single abusive user or a misconfigured client can spend your entire monthly budget in minutes.

The @upstash/ratelimit library makes per-user and per-IP limits composable:

// src/lib/ratelimit.ts
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const redis = Redis.fromEnv();

export const aiRouteLimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(20, '1 m'),
  analytics: true,
  prefix: 'ai_route',
});

export const authRouteLimit = new Ratelimit({
  redis,
  limiter: Ratelimit.fixedWindow(5, '1 m'),
  analytics: true,
  prefix: 'auth',
});
// src/app/api/chat/route.ts
import { aiRouteLimit } from '@/lib/ratelimit';
import { headers } from 'next/headers';

export async function POST(req: Request) {
  const ip = headers().get('x-forwarded-for') ?? '127.0.0.1';
  const { success, remaining } = await aiRouteLimit.limit(ip);

  if (!success) {
    return Response.json(
      { error: 'Rate limit exceeded. Try again in a moment.' },
      {
        status: 429,
        headers: { 'X-RateLimit-Remaining': remaining.toString() },
      }
    );
  }

  // Process AI request...
}

The analytics: true flag writes aggregated rate limit data to a separate Upstash Redis key that the Upstash console visualises. You can see which identifiers are hitting limits most often without adding any instrumentation code.

Claude generates a working implementation of this without the CLAUDE.md, but it reaches for a sorted-set Lua script instead of the library. That home-grown implementation has two common failure modes: it does not set the X-RateLimit-* headers that clients need to implement backoff, and its sliding window calculation has an off-by-one error on the window boundary that causes the limit to fire one request early or one request late under concurrent load.

Caching patterns for Next.js

Upstash Redis is well-suited for caching expensive queries, API responses, and computed values that should survive function cold starts.

// src/lib/cache.ts
import { Redis } from '@upstash/redis';

const redis = Redis.fromEnv();

export async function cached<T>(
  key: string,
  ttlSeconds: number,
  fn: () => Promise<T>
): Promise<T> {
  const cached = await redis.get<T>(key);
  if (cached !== null) return cached;

  const fresh = await fn();
  await redis.setex(key, ttlSeconds, JSON.stringify(fresh));
  return fresh;
}
// Usage in a Server Component or API route
const products = await cached(
  `products:${categoryId}`,
  300, // 5 minutes
  () => db.select().from(productsTable).where(eq(productsTable.categoryId, categoryId))
);

The caching wrapper handles null vs. cache miss correctly. Upstash Redis returns null (not undefined) for missing keys, so the !== null check is required. Claude generates if (!cached) which incorrectly treats the number 0 and the string "" as cache misses.

This is where Claudify matters: the CLAUDE.md ships with the !== null guard pre-specified so Claude Code generates correct null-safe cache reads from the first draft.

QStash for durable background jobs

QStash is an HTTP message queue that solves a specific problem: you want to process something after a response is sent, but your function timeout is too short to do it inline, and a regular job queue requires a persistent server.

// src/app/api/orders/route.ts
import { Client } from '@upstash/qstash';

const qstash = new Client({ token: process.env.QSTASH_TOKEN! });

export async function POST(req: Request) {
  const order = await req.json();

  // Immediately persist the order
  await db.insert(ordersTable).values(order);

  // Enqueue post-order processing for later
  await qstash.publishJSON({
    url: `${process.env.NEXT_PUBLIC_APP_URL}/api/process-order`,
    body: { orderId: order.id },
    retries: 3,
    delay: 0,
  });

  return Response.json({ orderId: order.id }, { status: 201 });
}
// src/app/api/process-order/route.ts
import { verifySignatureAppRouter } from '@upstash/qstash/nextjs';

async function handler(req: Request) {
  const { orderId } = await req.json();
  // Process order: send confirmation email, update inventory, notify warehouse
  await sendOrderConfirmation(orderId);
  return new Response('OK');
}

export const POST = verifySignatureAppRouter(handler);

The verifySignatureAppRouter wrapper validates the QStash HMAC signature before calling your handler. Without this, any attacker who discovers your endpoint URL can trigger arbitrary job processing. Claude omits signature verification by default because it generates generic POST handlers without awareness of QStash's security model.

QStash retries on non-2xx responses up to the retry count you specify. This means your handler must be idempotent: processing the same orderId twice should produce the same result as processing it once. The CLAUDE.md should declare idempotency requirements for all QStash handlers.

Common Upstash mistakes to avoid

Using TCP Redis in edge functions. The redis npm package opens a TCP socket. Cloudflare Workers and Vercel Edge Functions do not support TCP. The connection fails silently and the first Redis command hangs until it times out. Use @upstash/redis which operates over HTTP.

Not handling QStash retries. QStash retries failed deliveries. If your handler creates a side effect (sends an email, charges a card) and then throws before returning 200, QStash will retry and the side effect runs again. Either make handlers idempotent or use a deduplication key: qstash.publishJSON({ ..., deduplicationId: orderId }).

Key namespace collisions. Multiple features using Upstash Redis without key prefixes will collide. A get("user:123") in your caching layer will overwrite a set("user:123") in your session layer if both write to the same key. The CLAUDE.md should require feature-scoped prefixes on all Redis keys.

Forgetting to set analytics: true. The Upstash console's rate limit analytics tab only populates when analytics: true is set on the Ratelimit instance. It has no performance cost and provides the only visibility into which identifiers are being rate limited.

FAQ

What is the difference between UPSTASH_REDIS_URL and UPSTASH_REDIS_REST_URL?

UPSTASH_REDIS_URL is a rediss:// TCP connection string compatible with ioredis and the redis npm package. It works only in environments with Node.js TCP support. UPSTASH_REDIS_REST_URL is an https:// REST endpoint compatible with @upstash/redis. It works in any runtime. Use the REST URL in serverless and edge contexts, which is the majority of modern Next.js deployments.

Can Upstash Redis replace my Postgres database for session storage?

For session tokens (short-lived, key-value) yes. For user data with relational queries, no. A common pattern is Postgres (via Neon) for durable application data and Upstash Redis for ephemeral state: rate limit counters, session tokens, feature flags, and cache entries with short TTLs.

How does QStash compare to Inngest?

QStash is simpler: it is an HTTP message queue with retry logic. You publish a message and a URL; QStash delivers it. Inngest is a full durable execution framework with step functions, parallel fans, and event-driven workflows. Use QStash when you need "send this HTTP request and retry if it fails". Use Inngest when you need "run this multi-step workflow and resume after each step".

Get Claudify. Your CLAUDE.md includes the correct Upstash Redis client, rate limiting library, and QStash signature verification patterns so Claude Code generates production-safe serverless caching and queuing from the start.

Ready to upgrade your Claude Code setup?

Get Claudify
Featured on Dofollow.Tools AI Toolz Dir