← Back to dashboard
upstashdatabasefresh

Upstash (Redis + QStash) Integration Guide

What is Upstash?

The real model

@upstash/redis (HTTP), QStash (HTTPS queue), Vector (managed ANN), Kafka.

@upstash/redis is a thin HTTP wrapper — call it from Vercel Edge / Cloudflare Workers / any runtime without TCP. Global replication keeps p99 reads under ~30ms anywhere. QStash sends a signed POST to your HTTPS endpoint at a scheduled time or after a delay — perfect for cron-style jobs, delayed-callback retries, and offloading slow work from a webhook. The Vector product is a serverless Pinecone-ish ANN with similar pricing dynamics. For codeAmani, Upstash is the offload layer: M-Pesa callbacks ACK fast, then enqueue the slow part to QStash.

Five Upstash primitives

Same HTTPS contract everywhere. Pay per call, scale to zero.

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

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:

  • Upstash Redis (@upstash/redis) — a Redis-compatible KV store exposed as a REST API. Use it for caching, sessions, feature flags, and rate limiting (@upstash/ratelimit). Multi-region replication keeps latency low for East African edge traffic.
  • QStash (@upstash/qstash) — a serverless message queue + scheduler with guaranteed delivery and automatic retries. Use it to run background jobs, fan out webhooks, and schedule recurring work (cron) without managing a worker fleet.

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.

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

Official Documentation


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):

Bash
# 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

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

3. Redis client

TypeScript
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.

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

TypeScript
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;
}
TypeScript
// 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)

TypeScript
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:

TypeScript
"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:

TSX
// 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

  • Edge-safe: use Upstash (HTTP) rather than ioredis (TCP) anywhere on Vercel Edge / serverless. This is our default cache + queue on Vercel.
  • M-Pesa fit: QStash's guaranteed delivery + retries pair with the M-Pesa idempotency rule (store CheckoutRequestID, dedupe on callback) — offload reconciliation and notification jobs to QStash so the Daraja callback returns fast.
  • Rate limiting: put @upstash/ratelimit in front of STK-Push and auth routes to blunt abuse without standing up extra infra.
  • Security: all tokens and signing keys stay server-side (.env.local / Vercel env); always verify QStash signatures on receiver routes before acting on the payload.
  • Cost: pay-per-request + scale-to-zero matches low/spiky SME traffic — no idle cost.