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.
Console → Create database. Pick:
After creation, you'll see UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN. Add both to .env.
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:
ex = seconds). Without it you're permanent-caching, and stale data is worse than slow data.user:123:plan not user:123. You can invalidate one field without nuking the rest.redis.del(key). Don't rely on TTL alone for user-facing data.Full API: Upstash TS SDK docs. Mirrors Redis commands 1-to-1: get, set, incr, lpush, zadd, etc.
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:
Both SDKs work unchanged in edge runtimes — HTTP/REST means no TCP socket. Wire env vars in your platform:
wrangler secret put UPSTASH_REDIS_REST_URL + wrangler secret put UPSTASH_REDIS_REST_TOKEN.fly secrets set UPSTASH_REDIS_REST_URL=….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.
redis.del(cacheKey) on the write path, not just rely on TTL.UPSTASH_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: upstash.com/pricing.