From api-database-upstash
Upstash serverless Redis -- REST-based client, auto-serialization, pipelines, rate limiting, QStash, edge compatibility, global replication
How this skill is triggered — by the user, by Claude, or both
Slash command
/api-database-upstash:api-database-upstashThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> **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 objec...
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 viasetcome back as objects fromget), which is convenient but has gotchas with large numbers and cross-client compatibility. Useredis.pipeline()to batch commands into a single HTTP request,redis.multi()for atomic transactions, and@upstash/ratelimitfor pre-built rate limiting algorithms. For background jobs, use@upstash/qstashwhich pushes messages to your API via HTTP webhooks.
<critical_requirements>
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>
Additional resources:
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:
Key patterns covered:
@upstash/redis client setup with Redis.fromEnv() and constructor optionsredis.pipeline()) and atomic transactions (redis.multi())@upstash/ratelimit algorithms: sliding window, fixed window, token bucket@upstash/qstash for serverless background jobs and schedulingcontext.waitUntil() patternsWhen NOT to use:
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:
JSON.stringify/JSON.parse. This simplifies 90% of use cases but surprises developers who expect raw string behavior.redis.pipeline() to reduce round-trips.@upstash/ratelimit provides production-ready algorithms without writing Lua scripts. The library handles all the Redis plumbing internally.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
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: "[email protected]",
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
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", "[email protected]", { 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", "[email protected]");
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
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:
redis.pipeline()) -- Commands are independent, you want batching for speed, atomicity not requiredredis.multi()) -- Commands must all succeed together, no interleaving allowedPre-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
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
<decision_framework>
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 @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)
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
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>
High Priority Issues:
JSON.stringify() before passing objects to redis.set() -- auto-serialization already handles this, resulting in double-encoded strings like "{\"name\":\"Alice\"}" that break on readpending promise from ratelimit.limit() in edge runtimes -- analytics data and multi-region sync are lost silently; use context.waitUntil(pending)await redis.get/set() calls without pipelining -- each is a separate HTTP request, adding 25-75ms of unnecessary latencyredis.subscribe), blocking commands (BRPOP, BLPOP), or Lua scripting (eval) -- Upstash REST API does not support these; use ioredis with TCPMedium Priority Issues:
automaticDeserialization: false when interoperating with non-Upstash clients -- other clients store raw strings, Upstash will fail to parse them as JSONRedis instance per request instead of reusing a module-level singleton -- while connectionless, the client still benefits from HTTP keep-alive and warm connectionsCommon Mistakes:
redis.get() to return a string when an object was stored -- auto-deserialization returns the original object type, not a JSON stringredis.multi() for atomicityRatelimit.slidingWindow with MultiRegionRatelimit -- sliding window has high Redis command overhead in multi-region setups; use fixedWindow insteadGotchas & Edge Cases:
2^53 - 1 (Number.MAX_SAFE_INTEGER). Upstash returns these as strings even when the TypeScript type says number. Always validate large numeric values.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.redis.set("key", "value", { ex: 300 }) not redis.set("key", "value", "EX", 300) -- the ioredis positional argument style does not work.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.WATCH for optimistic locking. Use redis.multi() for atomic operations or implement application-level optimistic concurrency.enableAutoPipelining: true in the constructor.</red_flags>
<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>
npx claudepluginhub agents-inc/skills --plugin api-database-upstashProvides guides for using @upstash/redis SDK: data structures, pipelining, scripting, caching, rate limiting, sessions, and leaderboards.
Reads and writes to Upstash Redis-compatible key-value store via REST API. Supports strings, hashes, lists, sets, sorted sets, and other Redis data structures. Useful for caching, counters, and persistent storage.
Redis patterns for caching, rate limiting, distributed locks, session storage, Pub/Sub, and connection management in production applications.