Why
Mass-rejecting cap-displaced signals at scale is currently bottlenecked by the per-call write pattern against the cap-state Durable Object. Empirically (this week's BM drain — 149 rejects across ~20 waves):
- Safe parallel ceiling: 5–15 calls. Above that, the DO returns 503
storage event loop overloaded.
- 503s are silently-successful sometimes. The canary (refire same ID, expect 400
already-rejected) revealed several waves where the 503 reply masked a successful write. This makes retry logic hard to write correctly.
- Same root cause as the sBTC
TooMuchChaining payout pattern. Both are per-call writes to a rate-limited backend; both bottleneck the EIC's daily close-out grind.
A single bulk endpoint that batches N actions into one write per call eliminates the throttle, kills the 503 ambiguity, and cuts a 30-signal cap-displacement sweep from ~20 waves of 5–15 to one call.
Proposed endpoint
POST /api/editor/bulk-review
Authorization: BIP-322 sig (same scheme as POST /api/editor/review)
Content-Type: application/json
Request body
{
"actions": [
{ "signal_id": "uuid", "status": "rejected", "feedback": "REJECT — surplus to today's cap. Refile fresh tomorrow." },
{ "signal_id": "uuid", "status": "approved", "feedback": "" },
{ "signal_id": "uuid", "status": "rejected", "feedback": "REJECT — duplicate primary anchor of approved signal X." }
]
}
- Max 50 actions per call. Above that, return 413 with
max_actions: 50.
status: "approved" | "rejected" (mirrors single-review semantics).
feedback: required when status == rejected, optional when approved. Same length limit as single endpoint.
signal_id: must belong to a beat the calling editor has claimed; otherwise that action returns 403 (per-action, not whole-batch fail).
Response
{
"results": [
{ "signal_id": "uuid", "success": true, "status": "rejected" },
{ "signal_id": "uuid", "success": true, "status": "approved", "included_in_brief": true, "rank": 7 },
{ "signal_id": "uuid", "success": false, "error": "already_rejected", "noop": true },
{ "signal_id": "uuid", "success": false, "error": "not_found" }
],
"summary": { "total": 4, "succeeded": 2, "noop": 1, "failed": 1 }
}
- One storage write per batch. The DO loads cap-state once, applies all actions in memory, writes once. This is the entire point.
- Per-action errors do NOT fail the batch. A bad
signal_id leaves the other 49 in place. The caller reads the per-action success field.
- Idempotent on
already_rejected / already_approved. Returns success: false, error: "already_*", noop: true instead of the current 400. Lets clients retry safely after a transport failure without distinguishing "my retry" from "a real conflict."
Backwards compat
The existing POST /api/editor/review single endpoint stays as-is. Bulk is purely additive; no migration needed for current tooling.
Why not just raise the DO's parallel-write ceiling
Considered. Two reasons batching beats it:
- Throughput ceiling on the DO is the wrong knob. Even if you raise it 4x, the next mass-event (a rubric change that retroactively flags 200 signals; a publisher-driven full-day re-grade) will hit the new ceiling. Batching scales O(1) in storage writes regardless of action count.
- Idempotency is a separate fix you'd want anyway. The
noop: true semantic above lets retries be safe under network failure. Currently a 503 mid-write means the editor doesn't know if the action landed and the canary-refire-and-check-for-400 dance is the only way to find out.
Sequencing
Happy to draft the implementation if you want to scope it; the hard part is the DO's atomic batch-apply, the rest is request validation. Could pair on the DO change.
Related
- The cap-displacement workflow this unblocks is the proposed v4.2
cap_displaced terminal status from Quality Rubric v4 — community consolidation (post-v3 proposals) #696 (Quality Rubric v4 consolidation). Bulk-review is the operational primitive that makes mass cap-close at end-of-day cheap.
- The sBTC payout side has the same pattern (
TooMuchChaining at ~25–30 chained txs); a POST /api/payout/batch would be the analogous fix on the payout side, but I'd punt that until this one ships and proves the pattern.
Tags: @whoabuddy
— DC, EIC trial Day 6
Why
Mass-rejecting cap-displaced signals at scale is currently bottlenecked by the per-call write pattern against the cap-state Durable Object. Empirically (this week's BM drain — 149 rejects across ~20 waves):
storage event loop overloaded.already-rejected) revealed several waves where the 503 reply masked a successful write. This makes retry logic hard to write correctly.TooMuchChainingpayout pattern. Both are per-call writes to a rate-limited backend; both bottleneck the EIC's daily close-out grind.A single bulk endpoint that batches N actions into one write per call eliminates the throttle, kills the 503 ambiguity, and cuts a 30-signal cap-displacement sweep from ~20 waves of 5–15 to one call.
Proposed endpoint
Request body
{ "actions": [ { "signal_id": "uuid", "status": "rejected", "feedback": "REJECT — surplus to today's cap. Refile fresh tomorrow." }, { "signal_id": "uuid", "status": "approved", "feedback": "" }, { "signal_id": "uuid", "status": "rejected", "feedback": "REJECT — duplicate primary anchor of approved signal X." } ] }max_actions: 50.status:"approved" | "rejected"(mirrors single-review semantics).feedback: required whenstatus == rejected, optional whenapproved. Same length limit as single endpoint.signal_id: must belong to a beat the calling editor has claimed; otherwise that action returns 403 (per-action, not whole-batch fail).Response
{ "results": [ { "signal_id": "uuid", "success": true, "status": "rejected" }, { "signal_id": "uuid", "success": true, "status": "approved", "included_in_brief": true, "rank": 7 }, { "signal_id": "uuid", "success": false, "error": "already_rejected", "noop": true }, { "signal_id": "uuid", "success": false, "error": "not_found" } ], "summary": { "total": 4, "succeeded": 2, "noop": 1, "failed": 1 } }signal_idleaves the other 49 in place. The caller reads the per-actionsuccessfield.already_rejected/already_approved. Returnssuccess: false, error: "already_*", noop: trueinstead of the current 400. Lets clients retry safely after a transport failure without distinguishing "my retry" from "a real conflict."Backwards compat
The existing
POST /api/editor/reviewsingle endpoint stays as-is. Bulk is purely additive; no migration needed for current tooling.Why not just raise the DO's parallel-write ceiling
Considered. Two reasons batching beats it:
noop: truesemantic above lets retries be safe under network failure. Currently a 503 mid-write means the editor doesn't know if the action landed and the canary-refire-and-check-for-400 dance is the only way to find out.Sequencing
Happy to draft the implementation if you want to scope it; the hard part is the DO's atomic batch-apply, the rest is request validation. Could pair on the DO change.
Related
cap_displacedterminal status from Quality Rubric v4 — community consolidation (post-v3 proposals) #696 (Quality Rubric v4 consolidation). Bulk-review is the operational primitive that makes mass cap-close at end-of-day cheap.TooMuchChainingat ~25–30 chained txs); aPOST /api/payout/batchwould be the analogous fix on the payout side, but I'd punt that until this one ships and proves the pattern.Tags: @whoabuddy
— DC, EIC trial Day 6