Tutorials / Backend integrations / Cache and rate-limit with Upstash
📝 Written ● Intermediate Updated 2026-05-13

Cache and rate-limit with Upstash Redis

Upstash is serverless Redis over HTTP. No connection pool, no port to keep open, no idle box. Cache hot queries, throttle abusive callers, queue background work — pay only for requests.

Pick Upstash when

0
  • Pick Upstash when — you run on Vercel/Cloudflare/Lambda and can't keep a TCP Redis connection open across invocations. Their HTTP/REST API is the only Redis you can use from edge functions.
  • AlternativesRedis Cloud (the official option, TCP-only, $$), Redis on Railway/Fly/Render (you operate it, cheap), self-host on a VPS (cheapest, most work), Cloudflare KV (eventually consistent, edge-replicated, much narrower API).
  • Free tier — 10K requests/day, 256 MB. Enough to ship a real product.
  • Sign upconsole.upstash.com/login. GitHub login. No credit card required.

Create a database

1

Console → Create database. Pick:

  • Type — Regional (cheaper, single region, ~5–20ms) or Global (replicated to multiple regions, ~5ms anywhere, costs more)
  • Region — pick the one closest to your server, not your users (your app already routes user traffic)
  • Eviction — leave on. If you hit the memory limit, oldest keys get evicted instead of writes failing

After creation, you'll see UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN. Add both to .env.

Cache hot queries

2

Install the client:

npm install @upstash/redis

Wrap any expensive query:

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

const redis = Redis.fromEnv();

async function getTopProducts() {
  const cached = await redis.get<Product[]>("top-products");
  if (cached) return cached;

  const fresh = await db.query("SELECT * FROM products ORDER BY sales DESC LIMIT 20");
  await redis.set("top-products", fresh, { ex: 60 }); // 60s TTL
  return fresh;
}

Patterns that matter:

  • Always set a TTL (ex = seconds). Without it you're permanent-caching, and stale data is worse than slow data.
  • Cache the answer to a question, not the entire objectuser:123:plan not user:123. You can invalidate one field without nuking the rest.
  • Invalidate on write — when the source-of-truth row changes, redis.del(key). Don't rely on TTL alone for user-facing data.
  • Cache the negative — "no rows found" is also worth caching, briefly. Otherwise a 404 path becomes your slowest path.

Full API: Upstash TS SDK docs. Mirrors Redis commands 1-to-1: get, set, incr, lpush, zadd, etc.

Rate-limit any endpoint

3

Install the rate-limit helper:

npm install @upstash/ratelimit

Middleware that limits each IP to 10 requests per 10 seconds (sliding window):

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, // dashboard graphs
});

// Express
app.use(async (req, res, next) => {
  const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress;
  const { success, limit, remaining, reset } = await ratelimit.limit(ip);

  res.setHeader("X-RateLimit-Limit", limit);
  res.setHeader("X-RateLimit-Remaining", remaining);
  res.setHeader("X-RateLimit-Reset", reset);

  if (!success) {
    return res.status(429).json({ error: "Too many requests" });
  }
  next();
});

Limiter options:

  • fixedWindow(n, "1 m") — counts per fixed minute. Cheapest, but bursty at boundaries.
  • slidingWindow(n, "10 s") — smooth, prevents boundary bursts. Default.
  • tokenBucket(n, "1 s", capacity) — allows bursts up to capacity, refills at rate. Use for APIs that should allow short bursts.

Identifier choices:

  • IP — rough; defeated by VPNs/proxies; still catches scraper-grade abuse
  • User ID — preferred for authenticated routes
  • API key — required for public APIs; lets you set per-key limits
  • Both — separate limiters with different windows (e.g., "5/min per IP and 100/hour per user")

Use it from Cloudflare Workers / Vercel Edge

4

Both SDKs work unchanged in edge runtimes — HTTP/REST means no TCP socket. Wire env vars in your platform:

  • Vercel — Project → Settings → Environment Variables. The Upstash Vercel integration auto-populates them.
  • Cloudflare Workerswrangler secret put UPSTASH_REDIS_REST_URL + wrangler secret put UPSTASH_REDIS_REST_TOKEN.
  • Fly.iofly secrets set UPSTASH_REDIS_REST_URL=….

Background jobs with QStash

5

If you also need delayed/scheduled work, QStash is the message queue on the same dashboard. It's an HTTP-based job runner: you POST a job, QStash retries until your endpoint returns 200.

import { Client } from "@upstash/qstash";

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

// Send "welcome email" 5 min after signup
await qstash.publishJSON({
  url: "https://yourapp.com/api/jobs/welcome-email",
  body: { userId: user.id },
  delay: 300, // seconds
});

Alternatives: Inngest (more workflow-shaped, better debugging UI), Trigger.dev, Temporal (enterprise). QStash is the simplest of the bunch.

Common failures

6
  • "Free tier exhausted" mid-day — 10K req/day = ~7 req/min. A rate-limiter that uses 2 Redis reads per request burns it fast. Either upgrade ($0.20/100K req) or cache the rate-limit check in-memory for short windows.
  • Stale cache after a write — your DB updated, but cache still serves old. Add a redis.del(cacheKey) on the write path, not just rely on TTL.
  • Rate limit hits real users behind shared NATs — colleges, corporate proxies, mobile carriers can put thousands of users behind one IP. For consumer apps, rate-limit by user ID once auth is established; only IP-limit unauth routes.
  • Multi-region: writes from US, reads from EU — Regional DB only lives in one region. Either pick the Global tier (replicated, ~$0.40/100K req) or move workloads to the same region as the DB.
  • Cold-start latency on Vercel — first Redis call after a cold start is ~80ms (TLS handshake + region routing). Subsequent calls are ~5ms. Don't measure your cache hit-rate on a single request.
  • Token in client codeUPSTASH_REDIS_REST_TOKEN is admin-level. Never expose to the browser. For client-side rate limiting use a server-side proxy or Upstash's read-only tokens.

Pricing reality

7
  • Free — 10K req/day, 256 MB, single region. Real-product viable.
  • Pay-as-you-go — $0.20 per 100K requests, $0.25/GB storage/mo, no minimum. Most small apps stay under $5/mo.
  • Global (multi-region) — $0.40 per 100K. Get if you need <20ms reads from anywhere.
  • Pro — $280/mo flat, includes 100M requests, dedicated resources. For when pay-as-you-go gets noisy.
  • QStash — separate billing: 500 messages/day free, $1 per 100K after.

Pricing: upstash.com/pricing.

Official references

Mental model. Redis is a key-value store with data structures (lists, sets, sorted sets, hashes, streams). Cache is just the most common use. Once you have it provisioned, you'll find a dozen places to use it: session storage, leaderboards, pub/sub, deduplication windows, feature-flag overrides.