前言
一句话总结:将重计算从 Worker 卸载到 DO 中。
我见网络上有零星的讨论提到可以将 CPU 任务卸载到 DO 中,但没找到一篇详细介绍的教程(也可能是我太火星了),官方文档里也没有类似的建议,能查到的主流用法都是维持 ws 长连接。我又问了一圈 AI,也没找到什么有价值的线索,它们给出的回答也不一致(甚至前后不一致),故来水一帖。
目前把帖子放 1 级是暂时防止 AI 爬到,看看佬友们有没有更好的向 AI 提问或是搜索方法。
为什么 Duration Object 可以缓解 CPU 时间限制
什么是 Duration Object
根据官方文档 What are Durable Objects?,原文如下:
A Durable Object is a special kind of Cloudflare Worker which uniquely combines compute with storage. Like a Worker, a Durable Object is automatically provisioned geographically close to where it is first requested, starts up quickly when needed, and shuts down when idle. You can have millions of them around the world. However, unlike regular Workers:
- Each Durable Object has a globally-unique name, which allows you to send requests to a specific object from anywhere in the world. Thus, a Durable Object can be used to coordinate between multiple clients who need to work together.
- Each Durable Object has some durable storage attached. Since this storage lives together with the object, it is strongly consistent yet fast to access.
Therefore, Durable Objects enable stateful serverless applications.
我的英语水平太过于塑料,就不翻译了,简单画一下重点:
- Worker 能跑的代码 DO 基本也能跑
- DO 是根据名称(id)来区分实例的,可以单例串行(多 Worker 共享)也可以多个实例并行
- DO 是直接有存储、有状态的
DO 是有状态的这一核心特征我们用不上,我们接着往下看文档。
计费与限制
Duration Object 的计费主要围绕计算和存储展开,计算部分的额度与计费规则如下:
| Free plan | Paid plan | |
|---|---|---|
| Requests | 100,000 / | 1 million, + $0.15/million |
| Includes HTTP requests, RPC sessions, WebSocket messages, and alarm invocations | ||
| Duration | 13,000 GB-s / | 400,000 GB-s, + $12.50/million GB-s |
其中有两条脚注需要特别注意:
4 Duration is billed in wall-clock time as long as the Object is active, but is shared across all requests active on an Object at once. Calling
accept()on a WebSocket in an Object will incur duration charges for the entire time the WebSocket is connected. It is recommended to use the WebSocket Hibernation API to avoid incurring duration charges once all event handlers finish running. For a complete explanation, refer to When does a Durable Object incur duration charges?.
Limits 怎么说:
| Feature | Limit |
|---|---|
| Number of Objects | Unlimited (within an account or of a given class) |
| Maximum Durable Object classes | 500 (Workers Paid) / 100 (Free) |
| Storage per account | Unlimited (Workers Paid) / 5GB (Free) |
| Storage per class | Unlimited |
| Storage per Durable Object | 10 GB |
| Key size | |
| Value size | |
| WebSocket message size | 32 MiB (only for received messages) |
| CPU per request | 30 seconds (default) / configurable to 5 minutes of active CPU time |
这里的 active CPU time 就是指的 CPU 时间,居然没有单独限制免费计划?!
我们再来跟 Worker 的限制对比一下:
| Feature | Workers Free | Workers Paid |
|---|---|---|
| Request | 100,000 requests/ 1000 requests/min | No limit |
| Worker memory | ||
| CPU time | 10 ms | 5 min HTTP request 15 min Cron Trigger |
| Duration | No limit | No limit for Workers. 15 min duration limit for Cron Triggers, Durable Object Alarms and Queue Consumers |
看起来,虽然 Worker 对免费计划有单独的 10ms 限制,但 DO 却没有,那理论上所有请求都用 DO 处理请求的话,平均 CPU 时间可以到 1.04s?这… 这对吗?
对不对一试便知
剧透一下,对的,从这里开始就是教程了
从一个最简单的 Demo 开始
我们来实现一个最简单的处理 HTTP 请求的 DO 试试。
定义 Durable Object 类
// src/index.ts import { DurableObject } from 'cloudflare:workers' // 1. 定义 DO 类,必须 export export class MyDurableObject extends DurableObject<Env> {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
}
// 2. 必须实现 fetch 方法,处理发给这个 DO 的请求 async fetch(request: Request): Promise<Response> {
// 在这里执行 CPU 密集型操作 const result = await this.heavyComputation();
return Response.json({ result });
}
private async heavyComputation(): Promise<string> {
// 你的 CPU 密集型逻辑 return "done";
}
}
在 wrangler.toml 中绑定
name = "do-cpu-test" main = "src/index.ts" compatibility_date = "2024-01-01" # 声明 Durable Object 绑定 [durable_objects] bindings = [
{ name = "MY_DO", class_name = "MyDurableObject" }
]
# 声明 DO 类的迁移(首次部署必须,免费计划只能使用 SQLite) [[migrations]] tag = "v1" new_sqlite_classes = ["MyDurableObject"]
定义 Env 类型
interface Env {
MY_DO: DurableObjectNamespace;
// 其他绑定...
}
从 Worker 调用 DO
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// 获取 DO 实例的 stub const id = env.MY_DO.idFromName("singleton");
const stub = env.MY_DO.get(id);
// 把请求转发给 DO return stub.fetch(request);
}
};
串行复用与并行处理
还记得之前说过 DO 是通过名称(id)来识别实例的么?我们可以通过传递 id 来选择是否创建新的实例。刚才的代码中名称固定是 "singleton",这样每次获取的都是同一个实例,一次只能处理一个请求,这在高并发下会排队,不过优势是可在 DO 内部维护状态(本文用不到)。
如果想并行处理:
每次请求都创建一个新的实例
可以直接使用 newUniqueId() 创建新 id。
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// newUniqueId() 每次返回全新的实例 const id = env.MY_DO.newUniqueId();
const stub = env.MY_DO.get(id);
return stub.fetch(request);
}
};
按需区分(如请求参数、用户 id)
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// 用 URL 参数作为实例名称 const instanceName = url.searchParams.get("name") || "default";
const id = env.MY_DO.idFromName(instanceName);
const stub = env.MY_DO.get(id);
return stub.fetch(request);
}
};
完整的示例
这里我实现了两个端点,分别在 Worker 和 DO 中执行 PBKDF2 迭代,来模拟实际 CPU 密集任务。
用法如下:
# Worker 内执行(很快会撞 CPU 限制)
curl "https://your-worker.workers.dev/worker/pbkdf2?iters=100000&reps=100" # DO 内执行(可以随便跑,reps拉倒1000都行)
curl "https://your-worker.workers.dev/do/pbkdf2?iters=100000&reps=100&name=bench" wrangler.toml
name = "do-cpu-test" main = "src/index.ts" compatibility_date = "2025-12-25" [durable_objects] bindings = [
{ name = "CPU_DO", class_name = "CpuDo" }
]
[[migrations]] tag = "v1" new_sqlite_classes = ["CpuDo"]
src/index.ts
/// <reference types="@cloudflare/workers-types" /> import { DurableObject } from 'cloudflare:workers' export interface Env {
CPU_DO: DurableObjectNamespace;
}
// ============ 工具函数:PBKDF2 烧 CPU ============ async function burnCpu(iters: number, reps: number) {
const password = "benchmark-password";
const salt = crypto.getRandomValues(new Uint8Array(16));
const enc = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
"raw",
enc.encode(password),
"PBKDF2",
false,
["deriveBits"]
);
const t0 = performance.now();
let lastHash = "";
for (let r = 0; r < reps; r++) {
const bits = await crypto.subtle.deriveBits(
{
name: "PBKDF2",
salt,
iterations: iters,
hash: "SHA-256",
},
keyMaterial,
256
);
lastHash = [...new Uint8Array(bits)]
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
return {
params: { iters, reps, totalIterations: iters * reps },
timing: { wallMs: Math.round(performance.now() - t0) },
output: lastHash,
};
}
// ============ Durable Object 类 ============ export class CpuDO extends DurableObject<Env> {
constructor(ctx: DurableObjectState,
env: Env) {
super(ctx, env);
}
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
const iters = Math.min(parseInt(url.searchParams.get("iters") || "100000"), 100000);
const reps = Math.min(parseInt(url.searchParams.get("reps") || "1"), 1000);
const result = await burnCpu(iters, reps);
return Response.json({
executor: "DurableObject",
instanceId: this.ctx.id.toString(),
...result,
});
}
}
// ============ Worker 入口 ============ export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const path = url.pathname;
// 路由:Worker 内执行 if (path === "/worker/pbkdf2") {
const iters = Math.min(parseInt(url.searchParams.get("iters") || "100000"), 100000);
const reps = Math.min(parseInt(url.searchParams.get("reps") || "1"), 1000);
const result = await burnCpu(iters, reps);
return Response.json({ executor: "Worker", ...result });
}
// 路由:DO 内执行 if (path === "/do/pbkdf2") {
const name = url.searchParams.get("name") || "default";
const id = env.CPU_DO.idFromName(name);
const stub = env.CPU_DO.get(id);
return stub.fetch(request);
}
return Response.json({
endpoints: {
"/worker/pbkdf2?iters=N&reps=M": "在 Worker 内执行(受 10ms CPU 限制)",
"/do/pbkdf2?iters=N&reps=M&name=X": "在 DO 内执行(按 Duration 计费)",
},
});
},
};
package.json
{ "name": "do-cpu-test", "scripts": { "dev": "wrangler dev", "dev:remote": "wrangler dev --remote", "deploy": "wrangler deploy" }, "devDependencies": { "@cloudflare/workers-types": "^4.20251201.0", "typescript": "^5.7.3", "wrangler": "^4.34.0" } } 测试结果
我直接把 reps 拉到 1000,除了玄学 1101 外没遇到别的限制。
这个 CPU 时间是 free plan 的 Worker 跑到冒烟都跑不出来的。
结论
现在可以回答开头的问题了:这对,DO 真的是 30s CPU 时间,只是 DO 还可能会受一些别的限制,比如释放不及时、同一台物理机上的 DO 会共用 128M 内存、创建 DO 和往返需要额外挂钟时间等。
现在我们来通俗的总结一下免费计划中 Worker 和 DO 的区别,这就像是占着茅
还是文雅的总结一下好了,这就像是在一家会员制餐厅里:
- Worker 是点餐的:点了多少才能吃多少(CPU 时间限制),但是这个座位想占多久占多久(挂钟时间不限制)
- Durable Object 是吃自助餐的:按座位数 × 时间算钱,但占着座位的时候可以一直猛猛吃。
无脑 DO 不可取,但适当利用可以极大拓展免费版 Worker 的可能,比如可以采用自实现的高迭代次数的密码 hash、解析大体积 JSON… 不知各位在用 Worker 构建项目时是否也遇到了 CPU 时间限制的困扰呢?也许 DO 就是你项目的最后一块拼图。
附言
这里我贴上各家 AI 对「DO 能否绕过 Worker 的 10ms 限制,是否有人这么尝试」的回答。
prompt:
CloseAI (gpt-5.2 Extended)
原回答很长,仅节选信息提取部分
评价:答案正确,信息来源提取无关,这两个都是后台任务,前者没用到 DO,后者更是非 CPU 密集任务,个人会将其评价为无效回答。
Gemini (3 Pro)
刚好测试的时候出 AB Test 了,我就截全了。
评价:回答正确,但是没有提供能立即验证的实际来源,也不知道到底有没有搜到正确的信息。右边算无效回答,不过左边直接给出 test 好评,个人将其评价为需要一点时间验证的有效回答。
Grok (免费版 Expert)
评价:搜索无敌,思考左右脑互搏,回答错误,这很 Grok。红框处的推明确提到可以用 DO 来 offload compute,你真就不自我怀疑一下的么… 评价为有用的错误回答。
@bcjordan@Cloudflare Durable Objects are effectively long-running, stateful JS servers: you can offload compute and/or stream results to/from with WebSockets as well — developers.cloudflare.com/durable-object…
Used a lot for multiplayer apps/coordination/long running tasks.
Perplexity (gpt-5.2-thinking + 搜索更多来源提示词)
评价:居然回答正确且来源正确。虽然 Markus Ast 的文章主要在说 ws 集成,跟 CPU 密集型任务的时间限制关系不大,但 git-on-cloudflare 里真的提到了:
- Time-budgeted unpacking keeps pushes under 30s CPU limit
以及
- 30s CPU limit per request on the main fetch paths (unpack runs in alarm-driven slices)
这个结果出乎我意料,其他家的 AI 试了好几次都搜不到啥有用信息。
Cursor (gpt-5.2 xhigh)
Cursor 的 prompt 不同,我是直接贴出当前项目中的两个有性能瓶颈的端点,让它分析能否用 DO 解决。
评价:路边一条。中间思维链中明明发现了信息又冲突,但它都不愿意自我怀疑一下,我也不知道官方文档里什么东西干扰它的判断了… 而且我开的是 Agent 模式,为啥这么不愿意写一个 test 呢…








评论区(暂无评论)