Cloudflare Workers Limits Audit
Sources reviewed:
relay/cf/src/workspace-doc.ts— Durable Object brokerapi/src/routers/*.ts— REST handlers (Hono workers;workspaces.ts,misc.ts,llm.ts,telemetry.ts,p2p.ts,internal.ts)mcp/src/index.ts+realtime.ts+tools/*.ts— MCP worker
CF limits from: Cloudflare Workers documentation (paid / Workers Paid plan assumed).
Limit-by-Limit Assessment
1. CPU Time per Request
Limit: 50 ms (free) / 30 s (paid) per invocation.
| Handler / path | Work done | Risk |
|---|---|---|
WebSocket webSocketMessage |
Decodes frame → validateUpdate (Zod + Y.Doc clone) + Y.applyUpdate + SQLite append |
Medium — validateUpdate clones the Y.Doc (proportional to doc size). At 10k jobs that clone alone is ~20 ms. |
applyOpRequest (DO REST, used by MCP) |
Two Y.Doc clones (probe + canonical) + Zod validation | Medium — same clone cost, times two. |
alarm() → maybeCompact + D1 backup |
Y.encodeStateAsUpdate + SQLite write + D1 UPDATE |
Low/Medium — I/O-bound; CPU portion is the encode step. At 10k jobs encode is ~5 ms. |
GET /api/workspaces/:id |
D1 SELECT + JSON.parse + maskCloudKeys | Low — O(1) I/O, trivial CPU. |
POST /api/workspaces |
D1 INSERT x2 + migrateProfileMap (one-off) |
Low — migration runs once per workspace. |
GET/POST /api/workspaces/:id/jobs |
D1 SELECT/INSERT + optional DO service-binding call | Low — all I/O. |
POST /api/proxy |
Single outbound fetch with 30 s timeout |
Low CPU, High wall time — the 30 s timeout is fine on paid but blocks the thread. |
MCP tools/call → applyOp service binding |
Zod schema parse + service-binding fetch to DO | Low — CPU work is tiny; latency dominated by the service-binding round-trip. |
Verdict: The validateUpdate / Y.Doc clone path is the only realistic CPU hot
spot. It is proportional to Y.Doc size. Under the paid 30 s budget this is not a
hard limit today, but at > 5 000 jobs it will consume noticeable CPU.
2. Subrequest Count
Limit: 50 (free) / 1 000 (paid) per request.
| Handler | Subrequests | Notes |
|---|---|---|
webSocketMessage |
1 (SQLite write, counted as storage I/O, not subrequest) | No external fetches. |
POST /api/workspaces/:id/jobs |
1 D1 SELECT + 1 D1 INSERT + 1 DO service-binding | 3 total. |
POST /api/proxy |
1 outbound fetch to allow-listed domain |
1 total. |
MCP tools/call (mutating) |
1 D1 query + 1 REALTIME service binding + 1 DO /ops/ internal |
3 total. |
MCP tailor tool |
D1 read + DO snapshot read (GET /ops/:wsId?keys=...) + D1 write |
~3 total. |
Verdict: No handler comes close to the 50-subrequest free-tier limit, let alone 1 000. No risk.
3. Memory per Isolate
Limit: 128 MB per Worker isolate (shared across all requests in the same isolate).
| Component | Memory usage | Notes |
|---|---|---|
| WorkspaceDoc DO — Y.Doc at 1k jobs | ~1.9 MB raw | Safe. |
| WorkspaceDoc DO — Y.Doc at 10k jobs | ~18.4 MB raw | Approaches 14% of limit — manageable but start watching. |
| WorkspaceDoc DO — Y.Doc probe clone (applyOpRequest) | Same as doc size | Doubles peak usage during op validation. |
| Web worker isolate | Minimal — no in-memory Y.Doc | Only JSON parsing. |
| MCP worker isolate | Minimal — no Y.Doc | Only JSON-RPC routing. |
Verdict: Medium risk at > 8 000 jobs per workspace. A Y.Doc for a 10k-job
workspace is ~18 MB; the probe clone doubles that to ~37 MB during applyOpRequest.
At 65k jobs the doc would hit 128 MB in isolation, leaving no room for the clone.
Evicting tailored_cv/tailored_cl from terminal jobs (see YJS-SIZE-BENCH.md)
reduces per-job overhead by ~70% and pushes the effective limit to ~200k jobs.
4. DO Storage Budget
Limit: ~5 GB per Durable Object on the paid plan (SQLite-backed).
Using the compressed figure of ~0.13 KB/job:
| Jobs | DO storage (compressed) |
|---|---|
| 10 000 | 1.3 MB |
| 100 000 | 12.6 MB |
| 1 M | 126 MB |
Even at 1 million jobs the DO storage cost is 126 MB — well under 5 GB.
The ops log (unapplied Yjs deltas between snapshots) adds headroom proportional to the compaction threshold (200 ops). Each op is a small delta; even at 200 ops the log is < 1 MB.
Verdict: Low risk. DO storage is not a binding constraint for any realistic workspace size.
5. WebSocket Message Size
Limit: 1 MB per WebSocket message.
| Message type | Max size | Notes |
|---|---|---|
| Sync step 2 (server → client) | Bounded by CATCHUP_CAP_BYTES = 2 MB |
Exceeds the 1 MB WS limit for lagged clients. The cap is applied before sending, but the cap itself is 2 MB — double the WS limit. |
broadcastUpdate |
Size of the incoming Yjs op delta | Deltas are typically < 1 KB for single-field edits. Tailored CV writes could be 1–2 KB. |
| Awareness update | Small (clientID + cursor position) | < 1 KB. |
Verdict: Low/Medium risk. Individual op deltas and awareness frames are well
under 1 MB. The catch-up path (CATCHUP_CAP_BYTES = 2 MB) exceeds the WS message
limit and would silently fail for a severely lagged client. The constant should be
lowered to 900 KB or the payload should be split across multiple frames.
6. Concurrent WebSocket Connections per DO
Limit: 32 000 theoretical; practically constrained by memory (each session entry + awareness state consumes ~1–2 KB).
CareerVector is a per-workspace DO. A workspace is accessed by 1–20 users in typical usage. The theoretical limit is irrelevant here. Even if a workspace became viral (enterprise team of 1 000 users), 1 000 concurrent WebSocket connections would consume ~2 MB of session overhead — trivial.
Verdict: No risk.
7. DO Alarm Precision
Limit: Alarms fire within ~30 s of the scheduled time; not real-time.
The D1 backup alarm is set hourly. The compaction alarm fires within the 5-minute threshold. Neither is latency-sensitive.
Verdict: No risk.
Risk Matrix
| Limit | Risk | Trigger condition |
|---|---|---|
| CPU time per request | Medium | Y.Doc > 5k jobs, validateUpdate clone > 20 ms |
| Subrequest count | Low | Max 3 subrequests per handler today |
| DO memory (128 MB) | Medium | Y.Doc raw size > 64 MB (~33k jobs without eviction) |
| DO storage (5 GB) | Low | Would need 40M+ jobs at current compression ratio |
| WebSocket message size (1 MB) | Low | Catch-up cap set at 2 MB — exceeds limit for lagged clients |
| Concurrent WS connections | Low | Per-workspace; 1–1000 users, far below 32k |
| Alarm precision | Low | Hourly backups, not latency-sensitive |
Top 3 Most-Likely-to-Hit Limits
#1 — DO Memory (Medium risk, ~33k jobs without mitigation)
The raw Y.Doc is loaded fully into memory on every DO wake. At 1.89 KB/job, a
33k-job workspace hits the 64 MB ceiling (half of 128 MB, leaving room for the
probe clone). With tailored_cv eviction this extends to ~80–100k jobs.
Mitigation:
- Evict
tailored_cv/tailored_clfrom terminal jobs (see YJS-SIZE-BENCH.md §1). - If per-workspace job counts routinely exceed 10k, split the Y.Doc: scalar job
fields in the main doc,
tailored_cv/tailored_clin a secondary per-job doc or R2 blob, fetched on-demand.
#2 — CPU Time (Medium risk, > 5k jobs under concurrent writes)
validateUpdate clones the full Y.Doc before applying each op. The clone is
O(doc size). Under a burst of concurrent writes from multiple clients (e.g., an
LLM pipeline that fires 20 ops in parallel), the CPU budget can be consumed quickly
at large workspace sizes.
Mitigation:
- Apply
tailored_cveviction (reduces clone cost ~70%). - Consider a validation mode that skips the full clone for known-safe op kinds
(e.g.,
job.updatewith only scalar fields), falling back to full-clone validation only for structural changes.
#3 — WebSocket Message Size (Low/Medium risk, catch-up path)
CATCHUP_CAP_BYTES = 2_000_000 (2 MB) in workspace-doc.ts exceeds the 1 MB WS
message size limit. A severely lagged client (not connected for > 5 minutes, with
heavy concurrent writes) could receive a catch-up payload that silently truncates.
Mitigation (one-line fix):
// workspace-doc.ts line 45
const CATCHUP_CAP_BYTES = 900_000; // Stay under 1 MB WS limit
Or: split the catch-up into multiple sync frames (more complex but avoids the silent truncation).
Non-Issues Worth Noting
- Subrequest chaining: The MCP worker sends one service-binding call to REALTIME, which makes one DO fetch. That is 2 subrequests total — nowhere near the 1 000 paid limit.
- DO storage: Extremely cheap at this scale. Not a factor.
- Workers script size: The hand-rolled JSON-RPC framing in the MCP worker
(
mcp/src/index.ts) is intentionally thin (~180 LoC). No bundle size risk.