dist/plugins/api-database-upstash/skills/api-database-upstash/SKILL.md
Upstash serverless Redis -- REST-based client, auto-serialization, pipelines, rate limiting, QStash, edge compatibility, global replication
npx skillsauth add agents-inc/skills api-database-upstashInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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
</patterns><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>
development
Material Design component library for Vue 3
development
VitePress 1.x — Vue-powered static site generator for documentation sites, built on Vite
tools
Docusaurus 3.x documentation framework — site configuration, docs/blog plugins, sidebars, versioning, MDX, swizzling, and deployment
development
TanStack Form patterns - useForm, form.Field, validators, arrays, linked fields, createFormHook, type safety