Skills api-baas-upstash
Upstash serverless Redis -- REST-based client, auto-serialization, pipelines, rate limiting, QStash, edge compatibility, global replication
git clone https://github.com/agents-inc/skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/agents-inc/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/dist/plugins/api-baas-upstash/skills/api-baas-upstash" ~/.claude/skills/agents-inc-skills-api-baas-upstash && rm -rf "$T"
dist/plugins/api-baas-upstash/skills/api-baas-upstash/SKILL.mdUpstash Patterns
Quick Guide: Upstash provides a REST/HTTP-based Redis client (
) 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@upstash/rediscome back as objects fromset), which is convenient but has gotchas with large numbers and cross-client compatibility. Usegetto batch commands into a single HTTP request,redis.pipeline()for atomic transactions, andredis.multi()for pre-built rate limiting algorithms. For background jobs, use@upstash/ratelimitwhich pushes messages to your API via HTTP webhooks.@upstash/qstash
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
, named constants)import type
(You MUST use
for initialization in production code -- never hardcode Redis.fromEnv()
or UPSTASH_REDIS_REST_URL
values)UPSTASH_REDIS_REST_TOKEN
(You MUST handle the
promise from pending
responses in edge runtimes -- use @upstash/ratelimit
on Vercel Edge/Cloudflare Workers or analytics data is lost)context.waitUntil(pending)
(You MUST use
when issuing 3+ independent commands in a single handler -- each command is a separate HTTP round-trip without pipelining)redis.pipeline()
(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 -- Client setup, commands, auto-serialization, pipeline, transactions
- Rate Limiting -- @upstash/ratelimit algorithms, middleware, analytics
- QStash -- Background jobs, scheduling, message publishing
Additional resources:
- 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:
client setup with@upstash/redis
and constructor optionsRedis.fromEnv()- Automatic JSON serialization/deserialization behavior and gotchas
- Pipeline batching (
) and atomic transactions (redis.pipeline()
)redis.multi()
algorithms: sliding window, fixed window, token bucket@upstash/ratelimit
for serverless background jobs and scheduling@upstash/qstash- Global Database architecture (primary + read regions, eventual consistency)
- Edge runtime compatibility and
patternscontext.waitUntil()
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:
- 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.
- Auto-serialization is default -- Objects go in, objects come out. No manual
/JSON.stringify
. This simplifies 90% of use cases but surprises developers who expect raw string behavior.JSON.parse - Pipeline for performance -- Without pipelining, N commands = N HTTP requests. Always batch independent commands with
to reduce round-trips.redis.pipeline() - Rate limiting as a first-class citizen --
provides production-ready algorithms without writing Lua scripts. The library handles all the Redis plumbing internally.@upstash/ratelimit - Push-based messaging -- QStash delivers messages TO your API via HTTP webhooks. No long-running consumer processes needed -- perfect for serverless.
<patterns>
Core Patterns
Pattern 1: Client Setup with Redis.fromEnv()
Initialize using environment variables for zero-config deployment. See examples/core.md for full examples including constructor options and timeout configuration.
// 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
// 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 for type-safe patterns and disabling auto-serialization.
// 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
// 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 for typed pipeline results.
// 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
// 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 for examples.
// 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 (
) -- Commands are independent, you want batching for speed, atomicity not requiredredis.pipeline() - Transaction (
) -- Commands must all succeed together, no interleaving allowedredis.multi()
Pattern 5: Rate Limiting with @upstash/ratelimit
Pre-built rate limiting that handles all Redis internals. See examples/rate-limiting.md for all algorithms, middleware integration, and analytics.
// 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 for scheduling, retries, and receiver verification.
// 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
before passing objects toJSON.stringify()
-- auto-serialization already handles this, resulting in double-encoded strings likeredis.set()
that break on read"{\"name\":\"Alice\"}" - Ignoring the
promise frompending
in edge runtimes -- analytics data and multi-region sync are lost silently; useratelimit.limit()context.waitUntil(pending) - Issuing 5+ sequential
calls without pipelining -- each is a separate HTTP request, adding 25-75ms of unnecessary latencyawait redis.get/set() - Attempting Pub/Sub (
), blocking commands (redis.subscribe
,BRPOP
), or Lua scripting (BLPOP
) -- Upstash REST API does not support these; use ioredis with TCPeval
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
when interoperating with non-Upstash clients -- other clients store raw strings, Upstash will fail to parse them as JSONautomaticDeserialization: false - Creating a new
instance per request instead of reusing a module-level singleton -- while connectionless, the client still benefits from HTTP keep-alive and warm connectionsRedis
Common Mistakes:
- Expecting
to return a string when an object was stored -- auto-deserialization returns the original object type, not a JSON stringredis.get() - Assuming pipeline execution is atomic -- pipelines batch for network efficiency but other clients can interleave; use
for atomicityredis.multi() - Using
withRatelimit.slidingWindow
-- sliding window has high Redis command overhead in multi-region setups; useMultiRegionRatelimit
insteadfixedWindow - 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 >
(Number.MAX_SAFE_INTEGER). Upstash returns these as strings even when the TypeScript type says2^53 - 1
. Always validate large numeric values.number - Base64 encoding by default: The SDK requests base64-encoded responses to handle edge cases. If you see garbled output like
, the response encoding is interfering -- checkdmFsdWU=
option.responseEncoding
returnsredis.get()
for missing keys, notnull
: This matters for TypeScript narrowing -- checkundefined
, not truthiness.result !== null- SET options use an object, not positional args: Upstash uses
notredis.set("key", "value", { ex: 300 })
-- the ioredis positional argument style does not work.redis.set("key", "value", "EX", 300) - 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.
returns an empty objecthgetall
for non-existent keys: Check{}
, notObject.keys(result).length === 0
.result === null
does not work on Cloudflare Workers: Cloudflare'sblockUntilReady()
behaves differently; useDate.now()
with manual retry logic instead.limit()- No WATCH command: Upstash REST API does not support
for optimistic locking. UseWATCH
for atomic operations or implement application-level optimistic concurrency.redis.multi() - Auto-pipelining is available: The SDK can automatically batch commands issued during the same event loop tick via
in the constructor.enableAutoPipelining: true
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
, named constants)import type
(You MUST use
for initialization in production code -- never hardcode Redis.fromEnv()
or UPSTASH_REDIS_REST_URL
values)UPSTASH_REDIS_REST_TOKEN
(You MUST handle the
promise from pending
responses in edge runtimes -- use @upstash/ratelimit
on Vercel Edge/Cloudflare Workers or analytics data is lost)context.waitUntil(pending)
(You MUST use
when issuing 3+ independent commands in a single handler -- each command is a separate HTTP round-trip without pipelining)redis.pipeline()
(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>