Upstash serverless Redis -- REST-based client, auto-serialization, pipelines, rate limiting, QStash, edge compatibility, global replication
Scanned 9/4/2026
Install to Claude Code
npx -y skills add majiayu000/claude-skill-registry-data --skill api-database-upstash --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Api Database Upstash?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/majiayu000-api-database-upstash)More formats (shields.io, HTML) on the badges page.
---
name: api-database-upstash
description: Upstash serverless Redis -- REST-based client, auto-serialization, pipelines, rate limiting, QStash, edge compatibility, global replication
---
# Upstash Patterns
> **Quick Guide:** Upstash provides a **REST/HTTP-based Redis client** (`@upstash/redis`) designed for serverless and edge runtimes where TCP connections are unavailable. Unlike ioredis/node-redis, every command is an HTTP request -- no persistent connections, no connection pools, no teardown. The client **automatically serializes/deserializes JSON** (objects stored via `set` come back as objects from `get`), which is convenient but has gotchas with large numbers and cross-client compatibility. Use `redis.pipeline()` to batch commands into a single HTTP request, `redis.multi()` for atomic transactions, and `@upstash/ratelimit` for pre-built rate limiting algorithms. For background jobs, use `@upstash/qstash` which pushes messages to your API via HTTP webhooks.
---
<critical_requirements>
## CRITICAL: Before Using This Skill
> **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
**(You MUST use `Redis.fromEnv()` for initialization in production code -- never hardcode `UPSTASH_REDIS_REST_URL` or `UPSTASH_REDIS_REST_TOKEN` values)**
**(You MUST handle the `pending` promise from `@upstash/ratelimit` responses in edge runtimes -- use `context.waitUntil(pending)` on Vercel Edge/Cloudflare Workers or analytics data is lost)**
**(You MUST use `redis.pipeline()` when issuing 3+ independent commands in a single handler -- each command is a separate HTTP round-trip without pipelining)**
**(You MUST NOT use Upstash for Pub/Sub, blocking commands (BRPOP, BLPOP, XREAD BLOCK), or Lua scripting -- REST API does not support these; use ioredis with a TCP connection instead)**
</critical_requirements>
---
## Examples
- [Core Patterns](examples/core.md) -- Client setup, commands, auto-serialization, pipeline, transactions
- [Rate Limiting](examples/rate-limiting.md) -- @upstash/ratelimit algorithms, middleware, analytics
- [QStash](examples/qstash.md) -- Background jobs, scheduling, message publishing
**Additional resources:**
- [reference.md](reference.md) -- Command cheat sheet, constructor options, environment variables, eviction policies
---
**Auto-detection:** Upstash, @upstash/redis, @upstash/ratelimit, @upstash/qstash, Redis.fromEnv, UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN, Ratelimit.slidingWindow, Ratelimit.fixedWindow, Ratelimit.tokenBucket, serverless Redis, edge Redis, REST Redis
**When to use:**
- Serverless functions (AWS Lambda, Vercel, Netlify) that cannot maintain TCP connections
- Edge runtimes (Cloudflare Workers, Vercel Edge, Fastly Compute) that only support HTTP
- Rate limiting API routes with pre-built algorithms (sliding window, fixed window, token bucket)
- Caching in serverless/edge where ioredis connection pooling is impractical
- Background job scheduling with QStash (push-based, no long-running consumers needed)
- Global read latency optimization via Upstash Global Database with read replicas
**Key patterns covered:**
- `@upstash/redis` client setup with `Redis.fromEnv()` and constructor options
- Automatic JSON serialization/deserialization behavior and gotchas
- Pipeline batching (`redis.pipeline()`) and atomic transactions (`redis.multi()`)
- `@upstash/ratelimit` algorithms: sliding window, fixed window, token bucket
- `@upstash/qstash` for serverless background jobs and scheduling
- Global Database architecture (primary + read regions, eventual consistency)
- Edge runtime compatibility and `context.waitUntil()` patterns
**When NOT to use:**
- Long-running servers with persistent connections (use ioredis -- lower latency per command via TCP)
- Pub/Sub, blocking commands, or Lua scripting (REST API does not support these)
- Write-heavy workloads on Global Database (writes always go to primary region)
- Latency-critical paths where per-command HTTP overhead (~5-15ms) is unacceptable (use ioredis with TCP for <1ms per command)
- Large payloads (>1 MB) -- REST API has payload size limits
---
<philosophy>
## Philosophy
Upstash exists because **serverless and edge runtimes cannot maintain TCP connections**. Traditional Redis clients (ioredis, node-redis) rely on persistent TCP sockets -- they fail in Cloudflare Workers, break in short-lived Lambda functions, and cannot run in browser/WebAssembly environments. Upstash replaces TCP with REST/HTTP, trading per-command latency (~5-15ms vs <1ms) for universal compatibility.
**Core principles:**
1. **Connectionless by design** -- Every command is a stateless HTTP request. No connection pools, no teardown, no connection limits. This is a feature, not a limitation.
2. **Auto-serialization is default** -- Objects go in, objects come out. No manual `JSON.stringify`/`JSON.parse`. This simplifies 90% of use cases but surprises developers who expect raw string behavior.
3. **Pipeline for performance** -- Without pipelining, N commands = N HTTP requests. Always batch independent commands with `redis.pipeline()` to reduce round-trips.
4. **Rate limiting as a first-class citizen** -- `@upstash/ratelimit` provides production-ready algorithms without writing Lua scripts. The library handles all the Redis plumbing internally.
5. **Push-based messaging** -- QStash delivers messages TO your API via HTTP webhooks. No long-running consumer processes needed -- perfect for serverless.
</philosophy>
---
<patterns>
## Core Patterns
### Pattern 1: Client Setup with Redis.fromEnv()
Initialize using environment variables for zero-config deployment. See [examples/core.md](examples/core.md) for full examples including constructor options and timeout configuration.
```typescript
// Good Example
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
// Reads UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN automatically
export { redis };
```
**Why good:** Zero-config, environment variables injected by platform (Vercel, Fly.io), no secrets in code
```typescript
// Bad Example
import { Redis } from "@upstash/redis";
const redis = new Redis({
url: "https://us1-merry-cat-12345.upstash.io",
token: "AXXXAAIgcDE...",
});
```
**Why bad:** Hardcoded credentials leak in version control, non-portable across environments
---
### Pattern 2: Automatic JSON Serialization
Upstash auto-serializes objects with `JSON.stringify` on write and `JSON.parse` on read. See [examples/core.md](examples/core.md) for type-safe patterns and disabling auto-serialization.
```typescript
// Good Example -- objects round-trip automatically
interface UserProfile {
name: string;
email: string;
loginCount: number;
}
const CACHE_TTL_SECONDS = 3600;
await redis.set<UserProfile>(
"user:123",
{
name: "Alice",
email: "alice@example.com",
loginCount: 42,
},
{ ex: CACHE_TTL_SECONDS },
);
// Returns typed object -- no JSON.parse needed
const user = await redis.get<UserProfile>("user:123");
// user is UserProfile | null
```
**Why good:** TypeScript generics provide type safety, no manual serialization, TTL set via options object
```typescript
// Bad Example -- unnecessary manual serialization
await redis.set("user:123", JSON.stringify({ name: "Alice" }));
const raw = await redis.get("user:123");
const user = JSON.parse(raw as string); // Double-serialized: "{\"name\":\"Alice\"}"
```
**Why bad:** Auto-serialization already calls `JSON.stringify` -- doing it manually results in double-encoded strings that return as escaped JSON
---
### Pattern 3: Pipeline Batching
Batch multiple commands into a single HTTP request. Without pipelining, each command is a separate round-trip (~5-15ms each). See [examples/core.md](examples/core.md) for typed pipeline results.
```typescript
// Good Example -- single HTTP request for all commands
const USER_TTL_SECONDS = 3600;
const pipe = redis.pipeline();
pipe.set("user:123:name", "Alice", { ex: USER_TTL_SECONDS });
pipe.set("user:123:email", "alice@example.com", { ex: USER_TTL_SECONDS });
pipe.incr("stats:signups");
const results = await pipe.exec<["OK", "OK", number]>();
// results[0] => "OK"
// results[1] => "OK"
// results[2] => 1 (incremented value)
```
**Why good:** Single HTTP round-trip for 3 commands, typed results with generics, named TTL constant
```typescript
// Bad Example -- 3 separate HTTP requests
await redis.set("user:123:name", "Alice");
await redis.set("user:123:email", "alice@example.com");
await redis.incr("stats:signups");
// 3 round-trips = ~15-45ms total vs ~5-15ms with pipeline
```
**Why bad:** Each `await` is a separate HTTP request, tripling latency in serverless where every millisecond of cold start matters
---
### Pattern 4: Atomic Transactions
Use `redis.multi()` when commands must execute atomically. See [examples/core.md](examples/core.md) for examples.
```typescript
// Good Example -- atomic counter + flag update
const tx = redis.multi();
tx.incr("order:count");
tx.set("order:last-updated", Date.now());
const [count, status] = await tx.exec<[number, "OK"]>();
```
**Why good:** All commands execute atomically (no interleaving from other clients), typed results
**When to use pipeline vs transaction:**
- **Pipeline** (`redis.pipeline()`) -- Commands are independent, you want batching for speed, atomicity not required
- **Transaction** (`redis.multi()`) -- Commands must all succeed together, no interleaving allowed
---
### Pattern 5: Rate Limiting with @upstash/ratelimit
Pre-built rate limiting that handles all Redis internals. See [examples/rate-limiting.md](examples/rate-limiting.md) for all algorithms, middleware integration, and analytics.
```typescript
// Good Example
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
const MAX_REQUESTS = 10;
const WINDOW_DURATION = "10 s";
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(MAX_REQUESTS, WINDOW_DURATION),
analytics: true,
});
const { success, limit, remaining, reset, pending } =
await ratelimit.limit("user:123");
// CRITICAL: In edge runtimes, handle the pending promise
// context.waitUntil(pending);
if (!success) {
return new Response("Too Many Requests", {
status: 429,
headers: {
"X-RateLimit-Limit": String(limit),
"X-RateLimit-Remaining": String(remaining),
"X-RateLimit-Reset": String(reset),
},
});
}
```
**Why good:** No Lua scripts needed, named constants for limits, analytics for monitoring, proper 429 response with standard headers
---
### Pattern 6: QStash Background Jobs
Push-based messaging for serverless. See [examples/qstash.md](examples/qstash.md) for scheduling, retries, and receiver verification.
```typescript
// Good Example -- publish a background job
import { Client } from "@upstash/qstash";
const qstash = new Client({
token: process.env.QSTASH_TOKEN!,
});
await qstash.publishJSON({
url: "https://your-app.com/api/process-order",
body: { orderId: "order-456", action: "fulfill" },
retries: 3,
delay: "10s",
});
```
**Why good:** Fire-and-forget from handler, automatic retries on failure, configurable delay, at-least-once delivery guaranteed
</patterns>
---
<decision_framework>
## Decision Framework
### Upstash vs ioredis/node-redis
```
Which Redis client should I use?
|-- Running in edge runtime (Cloudflare Workers, Vercel Edge)?
| --> @upstash/redis (only option -- no TCP available)
|-- Running in serverless (Lambda, Vercel Serverless)?
| |-- Short-lived functions with no connection reuse?
| | --> @upstash/redis (no connection management overhead)
| |-- Long-lived functions with connection pooling?
| --> ioredis (lower per-command latency)
|-- Running on a persistent server (Docker, EC2, K8s)?
| --> ioredis (persistent TCP = <1ms latency vs ~5-15ms HTTP)
|-- Need Pub/Sub, blocking commands, or Lua scripts?
| --> ioredis (REST API cannot support these)
|-- Need to run in browser or WebAssembly?
--> @upstash/redis (HTTP works everywhere)
```
### Which Rate Limiting Algorithm?
```
Which @upstash/ratelimit algorithm should I use?
|-- Need strict, evenly distributed limiting?
| --> slidingWindow -- smoothest, no burst-at-boundary issues
|-- Need simple, low-overhead limiting?
| --> fixedWindow -- cheapest computationally, allows boundary bursts
|-- Need to allow burst traffic up to a capacity?
| --> tokenBucket -- smooths bursts, allows initial spike up to maxTokens
|-- Need multi-region rate limiting?
--> fixedWindow (slidingWindow has high Redis command overhead in multi-region)
```
### Pipeline vs Transaction vs Sequential
```
How should I batch these Redis commands?
|-- Commands are independent (no ordering dependency)?
| --> Pipeline (redis.pipeline()) -- non-atomic but single HTTP request
|-- Commands must execute atomically (all-or-nothing)?
| --> Transaction (redis.multi()) -- atomic, single HTTP request
|-- Only 1-2 commands?
--> Sequential is fine -- pipeline overhead not worth it
```
### Global Database vs Regional
```
Should I use Upstash Global Database?
|-- Read-heavy workload with users worldwide?
| --> Global Database -- reads from nearest replica
|-- Write-heavy workload?
| --> Regional Database -- writes always go to primary, replication doubles write cost
|-- Need strong consistency?
| --> Regional Database -- Global is eventually consistent
|-- Latency-sensitive reads from multiple continents?
--> Global Database -- sub-1ms reads from nearest region
```
</decision_framework>
---
<red_flags>
## RED FLAGS
**High Priority Issues:**
- Using `JSON.stringify()` before passing objects to `redis.set()` -- auto-serialization already handles this, resulting in double-encoded strings like `"{\"name\":\"Alice\"}"` that break on read
- Ignoring the `pending` promise from `ratelimit.limit()` in edge runtimes -- analytics data and multi-region sync are lost silently; use `context.waitUntil(pending)`
- Issuing 5+ sequential `await redis.get/set()` calls without pipelining -- each is a separate HTTP request, adding 25-75ms of unnecessary latency
- Attempting Pub/Sub (`redis.subscribe`), blocking commands (`BRPOP`, `BLPOP`), or Lua scripting (`eval`) -- Upstash REST API does not support these; use ioredis with TCP
**Medium Priority Issues:**
- Missing TTL on cached keys -- same as any Redis: unbounded memory growth until eviction kicks in
- Using Global Database for write-heavy workloads -- writes always route to primary region and replication doubles command costs
- Not setting `automaticDeserialization: false` when interoperating with non-Upstash clients -- other clients store raw strings, Upstash will fail to parse them as JSON
- Creating a new `Redis` instance per request instead of reusing a module-level singleton -- while connectionless, the client still benefits from HTTP keep-alive and warm connections
**Common Mistakes:**
- Expecting `redis.get()` to return a string when an object was stored -- auto-deserialization returns the original object type, not a JSON string
- Assuming pipeline execution is atomic -- pipelines batch for network efficiency but other clients can interleave; use `redis.multi()` for atomicity
- Using `Ratelimit.slidingWindow` with `MultiRegionRatelimit` -- sliding window has high Redis command overhead in multi-region setups; use `fixedWindow` instead
- Storing values larger than 1 MB -- REST API has payload size limits; store references and fetch large data from object storage
**Gotchas & Edge Cases:**
- **Large numbers become strings**: JavaScript cannot safely handle numbers > `2^53 - 1` (Number.MAX_SAFE_INTEGER). Upstash returns these as strings even when the TypeScript type says `number`. Always validate large numeric values.
- **Base64 encoding by default**: The SDK requests base64-encoded responses to handle edge cases. If you see garbled output like `dmFsdWU=`, the response encoding is interfering -- check `responseEncoding` option.
- **`redis.get()` returns `null` for missing keys, not `undefined`**: This matters for TypeScript narrowing -- check `result !== null`, not truthiness.
- **SET options use an object, not positional args**: Upstash uses `redis.set("key", "value", { ex: 300 })` not `redis.set("key", "value", "EX", 300)` -- the ioredis positional argument style does not work.
- **Global Database is eventually consistent**: A write followed immediately by a read from a different region may return stale data. Design for eventual consistency or use regional database for strong consistency.
- **`hgetall` returns an empty object `{}` for non-existent keys**: Check `Object.keys(result).length === 0`, not `result === null`.
- **`blockUntilReady()` does not work on Cloudflare Workers**: Cloudflare's `Date.now()` behaves differently; use `limit()` with manual retry logic instead.
- **No WATCH command**: Upstash REST API does not support `WATCH` for optimistic locking. Use `redis.multi()` for atomic operations or implement application-level optimistic concurrency.
- **Auto-pipelining is available**: The SDK can automatically batch commands issued during the same event loop tick via `enableAutoPipelining: true` in the constructor.
</red_flags>
---
<critical_reminders>
## CRITICAL REMINDERS
> **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
**(You MUST use `Redis.fromEnv()` for initialization in production code -- never hardcode `UPSTASH_REDIS_REST_URL` or `UPSTASH_REDIS_REST_TOKEN` values)**
**(You MUST handle the `pending` promise from `@upstash/ratelimit` responses in edge runtimes -- use `context.waitUntil(pending)` on Vercel Edge/Cloudflare Workers or analytics data is lost)**
**(You MUST use `redis.pipeline()` when issuing 3+ independent commands in a single handler -- each command is a separate HTTP round-trip without pipelining)**
**(You MUST NOT use Upstash for Pub/Sub, blocking commands (BRPOP, BLPOP, XREAD BLOCK), or Lua scripting -- REST API does not support these; use ioredis with a TCP connection instead)**
**Failure to follow these rules will cause credential leaks, silent data loss in edge runtimes, unnecessary latency from sequential HTTP requests, and runtime errors from unsupported commands.**
</critical_reminders>
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!