关于 api-database-upstash
api-database-upstash 汇总在无 TCP 连接的无服务器和边缘运行时中使用 Upstash Redis、限流与后台任务的开发模式。内容涵盖 REST 客户端、命令批处理、事务及适用场景和限制。
主要功能
- 01通过 REST/HTTP 使用 Redis
- 02自动序列化和反序列化 JSON
- 03用 pipeline 批处理独立命令
- 04用 multi 执行原子事务
- 05提供滑动窗口、固定窗口和令牌桶限流模式
- 06使用 QStash 推送后台任务
适用场景
- 01在边缘运行时缓存数据
- 02为 API 路由设置请求限流
- 03合并多个 Redis 命令以减少请求往返
- 04通过 HTTP Webhook 安排后台任务
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ agents-inc/ skills --skill api-database-upstash安装后在 Claude Code 中输入/api-database-upstash确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
metadata.yamlreference.mdexamples/core.mdqstash.mdrate-limiting.md详细介绍
Upstash 使用模式
快速指南: Upstash 提供基于 REST/HTTP 的 Redis 客户端(
@upstash/redis),专为无法使用 TCP 连接的无服务器和边缘运行时设计。与 ioredis/node-redis 不同,每条命令都是一个 HTTP 请求——没有持久连接、没有连接池,也无需拆除连接。客户端会自动序列化/反序列化 JSON(通过set存储的对象会在get时以对象形式返回),使用方便,但处理大数字和跨客户端兼容性时需要注意。使用redis.pipeline()将多条命令批量发送为一个 HTTP 请求,使用redis.multi()执行原子事务,并使用@upstash/ratelimit实现预构建的限流算法。对于后台任务,使用@upstash/qstash,它会通过 HTTP webhook 向你的 API 推送消息。
<critical_requirements>
重要:使用此技能前
所有代码都必须遵循 CLAUDE.md 中的项目约定(kebab-case、命名导出、导入顺序、
import type、命名常量)
(生产代码中必须使用 Redis.fromEnv() 进行初始化——绝不能将 UPSTASH_REDIS_REST_URL 或 UPSTASH_REDIS_REST_TOKEN 的值硬编码)
(在边缘运行时中,必须处理 @upstash/ratelimit 响应中的 pending promise——在 Vercel Edge/Cloudflare Workers 中使用 context.waitUntil(pending),否则分析数据会丢失)
(在单个处理程序中发出 3 条或更多相互独立的命令时,必须使用 redis.pipeline()——不使用管道时,每条命令都会单独产生一次 HTTP 往返)
(禁止将 Upstash 用于 Pub/Sub、阻塞命令(BRPOP、BLPOP、XREAD BLOCK)或 Lua 脚本——REST API 不支持这些功能;应改用通过 TCP 连接的 ioredis)
</critical_requirements>
示例
其他资源:
- reference.md -- 命令速查表、构造函数选项、环境变量、驱逐策略
自动检测: Upstash、@upstash/redis、@upstash/ratelimit、@upstash/qstash、Redis.fromEnv、UPSTASH_REDIS_REST_URL、UPSTASH_REDIS_REST_TOKEN、Ratelimit.slidingWindow、Ratelimit.fixedWindow、Ratelimit.tokenBucket、无服务器 Redis、边缘 Redis、REST Redis
适用场景:
- 无法维持 TCP 连接的无服务器函数(AWS Lambda、Vercel、Netlify)
- 仅支持 HTTP 的边缘运行时(Cloudflare Workers、Vercel Edge、Fastly Compute)
- 使用预置算法(滑动窗口、固定窗口、令牌桶)限制 API 路由的速率
- 在 ioredis 连接池不切实际的无服务器/边缘环境中进行缓存
- 使用 QStash 调度后台任务(基于推送,无需长期运行的消费者)
- 通过 Upstash Global Database 和读取副本优化全局读取延迟
涵盖的关键模式:
- 使用
Redis.fromEnv()和构造函数选项设置@upstash/redis客户端 - 自动 JSON 序列化/反序列化的行为与注意事项
- 管道批处理(
redis.pipeline())和原子事务(redis.multi()) @upstash/ratelimit算法:滑动窗口、固定窗口、令牌桶- 使用
@upstash/qstash处理无服务器后台任务和调度 - Global Database 架构(主区域 + 读取区域、最终一致性)
- 边缘运行时兼容性和
context.waitUntil()模式
不适用场景:
- 使用持久连接的长期运行服务器(使用 ioredis,通过 TCP 降低每条命令的延迟)
- Pub/Sub、阻塞命令或 Lua 脚本(REST API 不支持这些功能)
- Global Database 上写入密集型的工作负载(写入始终发送到主区域)
- 无法接受每条命令产生约 5–15 ms HTTP 开销的延迟关键路径(使用 ioredis,通过 TCP 将每条命令的延迟降至 1 ms 以下)
- 大型负载(>1 MB)——REST API 有负载大小限制
理念
Upstash 的存在,是因为无服务器和边缘运行时无法维持 TCP 连接。传统 Redis 客户端(ioredis、node-redis)依赖持久 TCP 套接字——它们在 Cloudflare Workers 中会失效,在生命周期短暂的 Lambda 函数中会出问题,也无法在浏览器/WebAssembly 环境中运行。Upstash 以 REST/HTTP 取代 TCP,以每条命令的延迟(约 5–15 ms,相比之下 TCP 低于 1 ms)换取广泛兼容性。
核心原则:
- 从设计上采用无连接模式——每条命令都是无状态的 HTTP 请求。无需连接池、拆除流程或连接数限制。这是特性,而非限制。
- 默认自动序列化——传入对象,取回对象。无需手动调用
JSON.stringify/JSON.parse。这简化了 90% 的用例,但对于期望原始字符串行为的开发者来说可能出乎意料。 - 使用管道提升性能——不使用管道时,N 条命令 = N 个 HTTP 请求。始终使用
redis.pipeline()批处理互不依赖的命令,以减少往返次数。 - 将速率限制作为一等功能——
@upstash/ratelimit提供可用于生产环境的算法,无需编写 Lua 脚本。该库会在内部处理所有 Redis 操作。 - 基于推送的消息传递——QStash 通过 HTTP Webhook 向你的 API 传递消息。无需长期运行的消费者进程——非常适合无服务器环境。
核心模式
模式 1:使用 Redis.fromEnv() 配置客户端
使用环境变量初始化,实现零配置部署。有关包含构造函数选项和超时配置的完整示例,请参阅 examples/core.md。
// Good Example
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
// Reads UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN automatically
export { redis };
为何推荐: 零配置,平台(Vercel、Fly.io)注入环境变量,代码中不含密钥
// Bad Example
import { Redis } from "@upstash/redis";
const redis = new Redis({
url: "https://us1-merry-cat-12345.upstash.io",
token: "AXXXAAIgcDE...",
});
为何不推荐: 硬编码凭据会泄露到版本控制中,且无法在不同环境间移植
模式 2:自动 JSON 序列化
Upstash 在写入时使用 JSON.stringify 自动序列化对象,在读取时使用 JSON.parse 自动反序列化。有关类型安全模式和禁用自动序列化的说明,请参阅 examples/core.md。
// 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
为何推荐: TypeScript 泛型提供类型安全,无需手动序列化,通过选项对象设置 TTL
// 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\"}"
为何不推荐: 自动序列化已经会调用 JSON.stringify——手动调用会导致双重编码,返回的字符串会转义 JSON
模式 3:Pipeline 批处理
将多个命令批量发送到单个 HTTP 请求中。不使用管道时,每条命令都需要单独往返一次(每次约 5-15ms)。有关带类型的 Pipeline 结果,请参阅 examples/core.md。
// 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)
为何推荐: 3 条命令只需一次 HTTP 往返,泛型提供带类型的结果,TTL 使用命名常量
// 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
为何不推荐: 每个 await 都会发起单独的 HTTP 请求,在每一毫秒冷启动时间都很重要的无服务器环境中,延迟会增加三倍
模式 4:原子事务
当命令必须原子执行时,使用 redis.multi()。示例请参阅 examples/core.md。
// 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"]>();
为何推荐: 所有命令均以原子方式执行(不会被其他客户端的操作插入),结果带有类型
何时使用 Pipeline,何时使用事务:
- Pipeline(
redis.pipeline())——命令彼此独立,需要通过批处理提升速度,无需保证原子性 - 事务(
redis.multi())——所有命令必须一起成功,不允许插入其他操作
模式 5:使用 @upstash/ratelimit 进行速率限制
开箱即用的速率限制工具,负责处理所有 Redis 内部细节。有关所有算法、中间件集成和分析功能,请参阅 examples/rate-limiting.md。
// 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),
},
});
}
为何推荐: 无需 Lua 脚本,使用命名常量设置限制,提供分析功能以便监控,并通过标准标头返回规范的 429 响应
模式 6:QStash 后台作业
面向无服务器环境的推送式消息传递。有关调度、重试和接收方验证,请参阅 examples/qstash.md。
// 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",
});
为何推荐: 处理程序可以发起即发即弃的调用,失败时自动重试,可配置延迟,并保证至少一次投递
<decision_framework>
决策框架
Upstash 与 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 @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、事务还是顺序执行
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>
危险信号
高优先级问题:
- 在将对象传递给
redis.set()前使用JSON.stringify()——自动序列化已经会处理这一操作,导致字符串被双重编码,例如"{\"name\":\"Alice\"}",读取时会出错 - 在边缘运行时忽略
ratelimit.limit()返回的pendingPromise——分析数据和多区域同步会悄无声息地丢失;请使用context.waitUntil(pending) - 连续执行 5 次或更多
await redis.get/set()调用而不使用管道——每次都是独立的 HTTP 请求,会产生 25-75ms 的不必要延迟 - 尝试使用 Pub/Sub(
redis.subscribe)、阻塞命令(BRPOP、BLPOP)或 Lua 脚本(eval)——Upstash REST API 不支持这些功能;请通过 TCP 使用 ioredis
中优先级问题:
- 缓存键未设置 TTL——与任何 Redis 一样,会导致内存无限增长,直到触发驱逐
- 对写入密集型工作负载使用 Global Database——写入始终路由到主区域,且复制会使命令成本翻倍
- 与非 Upstash 客户端交互时未设置
automaticDeserialization: false——其他客户端会存储原始字符串,Upstash 会无法将其解析为 JSON - 每个请求都创建新的
Redis实例,而不是复用模块级单例——虽然无需连接,但客户端仍可从 HTTP keep-alive 和预热连接中受益
常见错误:
- 预期
redis.get()返回字符串,但存储的是对象——自动反序列化会返回原始对象类型,而不是 JSON 字符串 - 误以为管道执行具有原子性——管道通过批处理提高网络效率,但其他客户端仍可穿插执行;需要原子性时请使用
redis.multi() - 将
Ratelimit.slidingWindow与MultiRegionRatelimit搭配使用——滑动窗口在多区域设置中会造成较高的 Redis 命令开销;请改用fixedWindow - 存储大于 1 MB 的值——REST API 有载荷大小限制;请存储引用,并从对象存储中获取大型数据
注意事项与边缘情况:
- 大数会变成字符串:JavaScript 无法安全处理大于
2^53 - 1(Number.MAX_SAFE_INTEGER)的数字。即使 TypeScript 类型声明为number,Upstash 也会以字符串形式返回这些值。请务必验证较大的数值。 - 默认进行 Base64 编码:SDK 会请求 Base64 编码的响应,以处理边缘情况。如果看到
dmFsdWU=这样的乱码输出,说明响应编码产生了干扰——请检查responseEncoding选项。 redis.get()对缺失的键返回null,而不是undefined:这会影响 TypeScript 类型收窄——请检查result !== null,不要使用真值判断。- SET 选项使用对象,而非位置参数:Upstash 使用
redis.set("key", "value", { ex: 300 }),而不是redis.set("key", "value", "EX", 300)——ioredis 的位置参数写法不起作用。 - Global Database 是最终一致的:写入后立即从其他区域读取,可能会返回过时数据。请针对最终一致性进行设计,或使用区域数据库以实现强一致性。
hgetall对不存在的键返回空对象{}:请检查Object.keys(result).length === 0,而不是result === null。blockUntilReady()在 Cloudflare Workers 上不起作用:Cloudflare 的Date.now()行为不同;请改用limit()并手动实现重试逻辑。- 没有 WATCH 命令:Upstash REST API 不支持用于乐观锁定的
WATCH。请使用redis.multi()执行原子操作,或在应用层实现乐观并发控制。 - 支持自动管道:SDK 可通过构造函数中的
enableAutoPipelining: true,自动批处理同一事件循环周期内发出的命令。
</red_flags>
<critical_reminders>
关键提醒
所有代码都必须遵循 CLAUDE.md 中的项目约定(kebab-case、命名导出、导入顺序、
import type、命名常量)
(在生产代码中初始化时必须使用 Redis.fromEnv()——绝不可硬编码 UPSTASH_REDIS_REST_URL 或 UPSTASH_REDIS_REST_TOKEN 的值)
(在边缘运行时中,必须处理 @upstash/ratelimit 响应中的 pending promise——在 Vercel Edge/Cloudflare Workers 上使用 context.waitUntil(pending),否则分析数据会丢失)
(在单个处理程序中发出 3 个或更多独立命令时,必须使用 redis.pipeline()——不使用管道时,每条命令都需要单独进行一次 HTTP 往返)
(绝不可将 Upstash 用于 Pub/Sub、阻塞命令(BRPOP、BLPOP、XREAD BLOCK)或 Lua 脚本——REST API 不支持这些功能;请改用通过 TCP 连接的 ioredis)
不遵循这些规则会导致凭据泄露、边缘运行时中的数据静默丢失、顺序 HTTP 请求造成不必要的延迟,以及不受支持的命令引发的运行时错误。