Upstash (Redis + QStash) Integration Guide

Technology: upstash · Category: database · Last reviewed: 2026-08-23

Source: https://tech-stack.codeamanilabs.org/guide/upstash

Insight:

Upstash is HTTP-based Redis + QStash, so both run on Vercel Edge where TCP clients like ioredis can't. Use Redis for caching and rate-limiting, QStash for background jobs — its guaranteed-delivery retries pair perfectly with the M-Pesa idempotency pattern, letting the Daraja callback return fast.

██╗   ██╗██████╗ ███████╗████████╗ █████╗ ███████╗██╗  ██╗
██║   ██║██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║  ██║
██║   ██║██████╔╝███████╗   ██║   ███████║███████╗███████║
██║   ██║██╔═══╝ ╚════██║   ██║   ██╔══██║╚════██║██╔══██║
╚██████╔╝██║     ███████║   ██║   ██║  ██║███████║██║  ██║
 ╚═════╝ ╚═╝     ╚══════╝   ╚═╝   ╚═╝  ╚═╝╚══════╝╚═╝  ╚═╝

Upstash (Redis + QStash) Integration Guide

Focus: Serverless Redis and the QStash message queue — both accessed over HTTP/REST, so they run on Vercel Edge / serverless functions where persistent TCP connections (e.g. ioredis) are not allowed. Pay-per-request pricing and scale-to-zero suit codeAmani's low-volume SME workloads.

Overview

Upstash gives two server-side primitives codeAmani uses:

Both authenticate with a token and require no persistent connection — ideal for the Next.js App Router / Vercel functions in our stack. Python SDKs (upstash-redis, qstash) exist if a service is written in Python.

Sibling products (same HTTP/REST contract). Upstash also ships Vector (@upstash/vector — serverless ANN for RAG) and Workflow (durable, multi-step serverless functions built on top of QStash). Upstash Kafka was discontinued on 2025-03-11 — Upstash steers former Kafka users to QStash / Workflow, so do not reach for Upstash Kafka in new builds.

Here is the big picture — both primitives reached over HTTP/REST from the same edge function:

flowchart LR
  APP["Vercel Edge<br/>Next.js function"] -->|"REST + token"| REDIS["Upstash Redis<br/>cache · sessions · rate-limit"]
  APP -->|"REST + token"| QSTASH["QStash<br/>queue + scheduler"]
  QSTASH -->|"deliver + retry"| WORKER["Receiver route<br/>background job"]

Official Documentation

Resource URL
Redis docs https://upstash.com/docs/redis
Redis TS SDK quickstart https://upstash.com/docs/redis/sdks/ts/getstarted
Rate limiting https://upstash.com/docs/redis/sdks/ratelimit-ts/overview
QStash docs https://upstash.com/docs/qstash
QStash Next.js quickstart https://upstash.com/docs/qstash/quickstarts/vercel-nextjs
Console https://console.upstash.com

1. Get credentials

Create a database / QStash instance at console.upstash.com, then copy the REST credentials into .env.local (and the Vercel project's env vars):

# Redis
UPSTASH_REDIS_REST_URL=https://<region>-<name>.upstash.io
UPSTASH_REDIS_REST_TOKEN=<token>
# QStash
QSTASH_TOKEN=<token>
QSTASH_CURRENT_SIGNING_KEY=<key>   # for verifying incoming messages
QSTASH_NEXT_SIGNING_KEY=<key>

The Vercel ↔ Upstash integration injects these automatically. Server-side only — never ship a token to the browser.

2. Install

npm install @upstash/redis @upstash/qstash   # JS/TS
# optional: npm install @upstash/ratelimit @upstash/vector
pip install upstash-redis qstash             # Python

Current versions (verified 2026-08-23): @upstash/redis 1.38.2, @upstash/qstash 2.11.3, @upstash/ratelimit 2.0.8, @upstash/vector 1.2.3; Python upstash-redis 1.7.0, qstash 3.4.0. Pin a range and re-check before a major bump — the API surface used here (Redis.fromEnv, Ratelimit.slidingWindow, Client.publishJSON, verifySignatureAppRouter) has been stable across these releases.

3. Redis client

import { Redis } from "@upstash/redis";

// Reads UPSTASH_REDIS_REST_URL + UPSTASH_REDIS_REST_TOKEN from the env
export const redis = Redis.fromEnv();

await redis.set("foo", "bar");
const bar = await redis.get<string>("foo");

The SDK auto-JSON.stringifys non-string values on set and parses them back on get<T>(), so you can store and read plain objects directly.

4. Caching pattern (cache-aside)

Caching is the primary reason to reach for Redis on Vercel. The cache-aside (lazy) pattern is: try the cache → on a miss compute the value, write it back with a TTL, then return. The TTL ({ ex: seconds }) caps how stale data can get and lets entries expire on their own — never cache without one.

flowchart TD
  START["request needs data"] --> GET["redis.get key"]
  GET --> HIT{"cache hit"}
  HIT -->|"yes"| RET["return cached value"]
  HIT -->|"no · miss"| COMPUTE["compute value<br/>db query · API call"]
  COMPUTE --> SET["redis.set key value<br/>ex = TTL seconds"]
  SET --> RET

A reusable helper — pass a key, a TTL in seconds, and a function that produces the value on a miss:

import { redis } from "@/lib/redis";

/**
 * Cache-aside: return the cached value, or compute it, store it with a TTL, and return it.
 * Values are JSON-serialised by the SDK, so T can be any JSON-safe shape.
 */
export async function cached<T>(
  key: string,
  ttlSeconds: number,
  compute: () => Promise<T>,
): Promise<T> {
  const hit = await redis.get<T>(key);
  if (hit !== null && hit !== undefined) {
    return hit; // cache hit
  }

  const value = await compute(); // miss — do the slow work once
  await redis.set(key, value, { ex: ttlSeconds }); // store with TTL (seconds)
  return value;
}
// Usage: cache an exchange rate for 5 minutes.
const rate = await cached("fx:usd-kes", 300, async () => {
  const res = await fetch("https://api.example.com/fx/usd-kes");
  return (await res.json()) as { rate: number };
});

Gotcha — cache stampede. When a hot key expires, many concurrent requests all miss at once and hammer the origin (DB / upstream API) in parallel before the first one repopulates the cache. For hot keys, mitigate with a short lock (set with nx as a mutex), a stale-while-revalidate window, or jittered TTLs so keys don't all expire on the same tick.

5. Rate limiting (protect API routes)

import { Ratelimit } from "@upstash/ratelimit";
import { redis } from "@/lib/redis";

const ratelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(10, "10 s"),
});

const { success } = await ratelimit.limit(userId);
if (!success) return new Response("Too many requests", { status: 429 });

6. QStash — publish a background job

The producer publishes once and QStash handles delivery — here is the full job lifecycle:

sequenceDiagram
  participant App as "Edge function"
  participant Q as "QStash"
  participant Route as "Receiver route"
  App->>Q: "publishJSON - url + body"
  Q-->>App: "messageId"
  Q->>Route: "POST signed message"
  Route->>Route: "verify signature"
  Route->>Route: "do the slow work"
  Route-->>Q: "200 ok"
  Note over Q,Route: "On failure QStash retries automatically"
"use server";
import { Client } from "@upstash/qstash";

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

export async function startBackgroundJob() {
  const { messageId } = await qstash.publishJSON({
    url: "https://<your-app>.vercel.app/api/long-task", // must be a public HTTPS URL
    body: { hello: "world" },
    // schedule instead of run-now:  cron: "0 9 * * *"
  });
  return messageId;
}

7. QStash — receive + verify the message

Always verify the signature so only QStash can trigger the endpoint:

// app/api/long-task/route.ts
import { verifySignatureAppRouter } from "@upstash/qstash/nextjs";

export const POST = verifySignatureAppRouter(async (req: Request) => {
  const body = await req.json();
  // ... do the slow work here
  return new Response("ok");
});

codeAmani notes

Official docs: