一句话总结:cf Agents SDK schedule(0, ...) self-renew 模式 + outbox.pushed_at 无索引导致全表 DELETE,叠加多 DO instance 并发,4 天产生 10.97T row reads,账单 $12,578。已于 5/3 12:13 修复(commit 6c95264 / ADR-0015)。


事故概况

字段 内容
项目 sandbox-agent-platform(基于 Cloudflare Workers + Durable Objects + Agents SDK 0.11.5)
异常时段 2026-04-29 ~ 2026-05-03(北京时间)
触发 commit e6a9790 wire UserAgent/ProjectAgent/BranchAgent to outbox + ADR-0008(4/29)
修复 commit 6c95264 adopt cf native stability — scheduleEvery 替代 self-renew(5/3 10:44)
修复部署 5/3 12:13 wrangler deploy (prod)
真正归零 5/4 01:33 之后停用,加上 fix 生效,用量降至 0
总损失 $12,578.22(DO Storage 占 99.95%)

影响范围($12,578 账单)

产品 总使用量 可计费使用量 使用成本
Durable Objects Storage Rows Read(前 25B 免费) 10.97T 10.95T $10,949.73
Durable Objects Storage Rows Written(前 50M 免费) 1.67B 1.62B $1,622.00
Container Memory, per GiB-Second(前 25 GiB-hours 免费) 2.69M 2.69M $6.49

关键指纹:读/写比 = 10.97T / 1.67B ≈ 6,569。健康 OLTP 系统这个比值通常 < 100;超过 1,000 基本就是「循环里反复扫一张没索引的表」。


时间线

4/29  e6a9790 部署 wire-d1-outbox
      ├── 每个 BranchAgent: onStart 中 schedule(0, sweepD1Outbox)
      ├── sweeper 内部 self-renew: schedule(result.nextDelaySec, sweepD1Outbox)
      └── nextDelaySec = (batch.length === 100) ? 0 : 30
              \\__ outbox 满 100 行 → 立即续 0 秒 → 死循环

4/30  你开始用 prod 跑 task
      ├── 04:17  PR flow / commit 自动化 bug 诊断(D1 远端查询)
      ├── 12:34  b8ead64 fix(pr-flow): no_diff 判据 → wrangler deploy
      ├── 21:04  c32be88 design docs 基建建立
      └── 单日账单首次破 $1,000

5/1   零星 hotfix + 单测基建
      └── 量级持续在 $1,000+/天

5/2   ★ 关键节点:dev/prod 环境拆分
      ├── 21:33  wrangler d1 create agent-platform-dev
      ├── 21:34  wrangler r2 bucket create agent-platform-{skills,workspaces}-dev
      ├── 21:34  vercel env add NEXT_PUBLIC_API_BASE_URL preview = <https://api-dev.douglas-agent.com>
      ├── 21:36  wrangler deploy --env dev
      ├── 21:44  git push origin main:dev → 触发 Vercel preview
      ├── 22:20  prod wipe(清 D1,但 DO 内 outbox 未清)
      └── 单日仍 $1,000+ — outbox 表持续累积

5/3   ★ 修复日
      ├── 03:00+  6 个 commit 改 cf-native stability(container 镜像、agent prompt 注入等)
      ├── 10:44  6c95264 feat(worker): adopt cf native stability
      │           - scheduleEvery(60, sweepD1Outbox) 替代 self-renew
      │           - keepAliveWhile 包 tickTask / handleTaskDone / triggerPrFlow
      │           - createBackup/restoreBackup
      ├── 12:13  wrangler deploy (prod) ──→ 🛑 死循环熄火
      ├── 12:38  wrangler deploy --env dev
      └── 之后多次 fix + dev 验证

5/4   00:30 ~ 01:33
      ├── milestone schema + Activity dedup + dev smoke fix
      ├── D1 migrations apply (dev + prod)
      ├── 01:33  最后一次 wrangler deploy (prod) + push main:dev
      └── 项目停用至 5/7

5/4 之后:用量归零(fix 生效 + 项目停用,双重原因)

根因分析

1. Cloudflare DO SQLite 计费模型

CF 的 row read 计费基于「SQLite 引擎扫描了多少行」,不是「SQL 返回多少行」。

SQL 5000 行表上的行为 计费
SELECT * WHERE seq = 100(seq 有索引) 走 PK 索引直接定位 1 row read
SELECT * WHERE seq > 0 ORDER BY seq LIMIT 100 走索引读 100 项 100 row read
DELETE WHERE pushed_at < X(pushed_at 无索引) 全表扫一遍才能判断 5000 row read

单价 $1.00 / 10 亿 row reads,听起来很便宜——但没有索引的查询每次执行都按全表大小计费,循环里跑就会爆。

2. Durable Object + alarm 运行模型