Limits
Every limit below is enforced at the cited file:line. Defaults are quoted; where a value is
env-tunable the variable name is in the table. Generated from the code; claims point at file:line.
Rate limits
Section titled “Rate limits”Every request first consumes a global budget and a spoof-resistant socket/client-IP budget. After
authentication, API-key and identity budgets are consumed independently; federation additionally
consumes a peer_id budget only after its HMAC proof succeeds (src/middleware/rate-limit.ts).
| Limit | Default | Source |
|---|---|---|
| Global gate | 20,000 req/min | CHORUS_RATE_LIMIT_GLOBAL_PER_MINUTE |
| Public-endpoint IP gate | 30 req/min | CHORUS_RATE_LIMIT_PUBLIC_IP_PER_MINUTE |
| Authenticated IP gate | 300 req/min | CHORUS_RATE_LIMIT_AUTHENTICATED_IP_PER_MINUTE |
| Federation peer | 100 req/min | CHORUS_RATE_LIMIT_FEDERATION_PEER_PER_MINUTE |
| Per-API-key and per-identity | 100 req/min each | DEFAULT_KEY_LIMIT |
| Admin authority multiplier | ×10 → 1000/min | ADMIN_MULTIPLIER |
| Per-key override | api_key.rate_limit |
applies to both key and identity checks for that request |
| Window | 60s fixed window | WINDOW_MS |
| Subject slots per scope | 4096 | CHORUS_RATE_LIMIT_SUBJECT_SLOTS |
“Public” is a fixed allowlist (/health, /invite, /auth-request,
/auth/challenge, /auth/siwe-login, /.well-known/agent-card.json, invite-skill); everything else
takes the authenticated IP gate. /health/detailed is authenticated and admin-gated.
Admitted responses carry X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset for
the most specific authority check. A denial also carries Retry-After and
RATE_LIMIT_EXCEEDED. If shared counter storage is unavailable, admission fails closed with
503 RATE_LIMIT_UNAVAILABLE.
Inbound body and JSON-RPC limits
Section titled “Inbound body and JSON-RPC limits”| Limit | Default | Env |
|---|---|---|
| Aggregate request body | 1 MiB | CHORUS_MAX_BODY_BYTES |
| Multipart artifact/workspace upload envelope | 64 MiB | CHORUS_MAX_UPLOAD_BODY_BYTES |
| RPC batch items | 32 | CHORUS_RPC_MAX_BATCH_ITEMS |
| RPC JSON nesting depth | 32 | CHORUS_RPC_MAX_JSON_DEPTH |
| RPC read-only notifications | 8 | CHORUS_RPC_MAX_NOTIFICATIONS |
| RPC aggregate cost | 100 | CHORUS_RPC_MAX_COST |
| RPC serialized response | 1 MiB | CHORUS_RPC_MAX_RESPONSE_BYTES |
| Federation proof lifetime | 60s (max 300s) | CHORUS_FEDERATION_PROOF_TTL_SECONDS |
| Federation verified response | 64 KiB | fixed in src/federation/v2/outbound.ts |
| Federation previous-key overlap | 300s (max 86,400s) | CHORUS_FEDERATION_PREVIOUS_KEY_OVERLAP_SECONDS |
The body guard counts every decoded stream chunk; Content-Length is only an early-rejection hint.
Overflow returns 413 HTTP_BODY_TOO_LARGE before parsing, authentication-side mutation, or route
dispatch. /mcp is dispatched separately and uses its own equivalent streamed body guard.
JSON routes enforce an explicit body state before their handler runs. Required bodies return
400 HTTP_BODY_REQUIRED when absent; empty-only actions return 400 HTTP_BODY_NOT_ALLOWED for
stray bytes. Optional JSON accepts only an absent or zero-byte body as {}. Malformed, trailing,
or non-UTF-8 JSON returns 400 HTTP_JSON_MALFORMED; missing, duplicate, or non-JSON media types
return 415 HTTP_MEDIA_TYPE_UNSUPPORTED; non-identity content encoding returns
415 HTTP_CONTENT_ENCODING_UNSUPPORTED. JSON body policies accept object roots only; null,
arrays, and scalar roots return 400 VALIDATION_INVALID_TYPE. These failures execute no command
and perform no write. Catalog DELETE actions default to empty-only unless they explicitly
declare a JSON body.
RPC preflight walks the whole payload before any handler. It rejects excess depth, batch size,
cost, notification count, projected response bytes, mutating notifications, multiple mutations,
and mixed read/mutation batches with RPC_PREFLIGHT_REJECTED. Unknown methods and malformed
read-only items retain normal JSON-RPC error behavior. Serialized responses are checked again
against the actual byte cap.
Ordinary REST JSON responses are also capped at 1 MiB. Catalog-backed mutations reserve
response-envelope space and reject near-limit inputs with RPC_PREFLIGHT_REJECTED before their
handler can write. If unpredictable server/database-derived expansion is discovered only after a
write commits, Chorus returns a successful CommittedMutationAcknowledgement instead of an error:
{accepted:true, committed:true, response_omitted:true, command_id, resource_id?}. The write is
durable and must not be retried; fetch the resource by resource_id when present. The TypeScript
SDK exports MutationResult<T> and isCommittedMutationAcknowledgement() for this branch. REST,
JSON-RPC, embedded MCP, and standalone MCP preserve it as a bounded success.
SSE limits
Section titled “SSE limits”The canonical change stream is GET /events/stream; GET /signals/stream is its signal-only compatibility view (see Signals & SSE).
| Limit | Default | Source |
|---|---|---|
| Concurrent streams / identity / replica | 5 | SSE_MAX_STREAMS_PER_IDENTITY |
| Durable stream retention | 10 min | SIGNAL_STREAM_RETENTION_MS |
| Catch-up page | 100 events | SSE_STREAM_PAGE_SIZE |
| Maximum pending lag | 2048 events | SSE_MAX_PENDING_EVENTS |
| Stream-ticket TTL | 60s, one use | STREAM_TICKET_TTL_MS |
| Keepalive comment | every 30s | SSE_KEEPALIVE_MS |
Over the stream limit returns 429 STREAM_LIMIT_EXCEEDED (routes.ts:96-103). Admins are exempt
from the concurrent-stream cap. Unknown and expired cursors fail explicitly with 409 and
reset_required: true; foreign cursors fail with 403.
Webhook limits
Section titled “Webhook limits”Delivery and retry are driven by src/webhooks/delivery.ts.
| Limit | Value | Source |
|---|---|---|
| Max delivery attempts | 3 | MAX_ATTEMPTS (delivery.ts:37) |
| Retry backoff | 5s, 30s, 5min | RETRY_DELAYS (delivery.ts:34) |
| Delivery timeout | 10s | DELIVERY_TIMEOUT_MS (delivery.ts:40) |
| Retry reaper interval | 60s | RETRY_INTERVAL_MS (delivery.ts:43) |
| Delivery purge limit | ≤ 1000 | validation.ts:94 |
min_urgency filter |
0–1 | validation.ts:25 |
After 3 failed attempts a delivery is moved to dead_letter and the webhook is flagged
failing:true with failure_count++ (delivery.ts:510-521). An inactive/deleted webhook or a
missing stored payload at retry time dead-letters immediately (delivery.ts:435-456). A purge call
must supply statuses (delivered/dead_letter) and/or before (validation.ts:92-94,98-101).
Object-storage limits
Section titled “Object-storage limits”| Limit | Default / maximum | Source |
|---|---|---|
| Guarded upload foreground deadline | 4 min inside a 5 min grace | src/object-storage/operations.ts |
| Guarded delete provider deadline | 20 s; maximum 25 s inside a 30 s grace | src/object-storage/operations.ts |
| General S3 request deadline | 30 s; configurable 1–900000 ms |
S3_REQUEST_TIMEOUT_MS |
| Direct S3 upload deadline | 270 s; configurable 1–900000 ms |
S3_UPLOAD_TIMEOUT_MS |
| Signed provider list body | 8 MiB | src/artifacts/storage.ts |
| Reconciliation claim page | 50; maximum 100 | src/object-storage/reconciler.ts |
| Claim pages per sweep | 4; maximum 100 | src/object-storage/reconciler.ts |
| Inventory page | 250; maximum 1000 | src/object-storage/inventory.ts |
| Inventory pages per sweep | 4; maximum 100 | src/object-storage/inventory.ts |
| Global unresolved operations | 10,000 | src/object-storage/operations.ts |
| Per-namespace unresolved operations | 1,000 | src/object-storage/operations.ts |
| Global unresolved uploads | 8,000 | src/object-storage/operations.ts |
| Per-namespace unresolved uploads | 800 | src/object-storage/operations.ts |
| Reconciliation attempts | 8 before dead letter | src/object-storage/operations.ts |
| Terminal operation retention | 30 days, bounded pages | src/object-storage/reconciler.ts |
| Inventory observation retention | 30 days, only after a complete scan | src/object-storage/reconciler.ts |
| Local MCP/CLI descriptor read | 50 MiB default; 0–1 GiB |
CHORUS_LOCAL_UPLOAD_MAX_BYTES |
An ambiguous upload consumes its exact reservation and unresolved capacity even after provider absence is observed; absence does not prove a timed-out PUT cannot arrive later. Delete accounting is released only after an exact successor/lease fence verifies provider absence and atomically settles its metadata. Inventory is non-destructive and cannot authorize a delete.
Durable memory-admin limits
Section titled “Durable memory-admin limits”| Limit | Value | Source |
|---|---|---|
| Import request lines | 1,000 | src/memory/admin-job.ts |
| Import request bytes / line bytes | 8 MiB / 128 KiB | src/memory/admin-job.ts |
| Immutable input chunk | 25 lines, 512 KiB | src/memory/admin-job.ts |
| Export total lines / source bytes | 100,000 / 64 MiB | src/memory/admin-job.ts |
| Export worker page | 10 memories | src/memory/admin-job.ts |
| Relations per exported memory | 1,000 | src/memory/admin-job.ts |
| Output line / chunk | 256 KiB / 512 KiB source JSONL | src/memory/admin-job.ts |
| Download page | default 100, maximum 500 lines | src/memory/admin-job.ts |
| Download encoded-byte budget | maximum 1 MiB plus one bounded hydration boundary chunk | src/memory/admin-job.ts |
| Namespace filters | 100, each at most 512 UTF-8 bytes | src/memory/admin-job.ts |
| Active import/export jobs | 32 | src/memory/admin-job.ts |
| Retained import/export job rows | 256 | src/memory/admin-job.ts |
| Import authority stripes | 256 | src/memory/admin-job.ts |
| Terminal job retention | 7 days, bounded pages | src/memory/admin-job.ts |
Completed export output is always paged. When the next encoded line cannot fit, the response is
413 MEMORY_EXPORT_PAGE_TOO_SMALL; callers must enlarge the page budget rather than advance the
cursor.
Pagination limits
Section titled “Pagination limits”Pagination is per-endpoint — there is no single global page size. The dominant Zod pattern for
list endpoints is default(20) with .max(100), e.g. artifacts and workspace lists. Circle
listing also defaults to 20 and caps at 100; visibility and active-window predicates are applied
before the database limit. Execution catalogs (tasks, runs, deliveries, checkouts, and
execution.diagnose) default to 100 and reject limits above 100 in both the public schema and
storage adapters. Non-admin task membership and API-key ring scope are storage predicates, so
hidden newer rows cannot consume a visible page before ORDER BY/LIMIT; the admin override
remains unfiltered. Run-scoped delivery and checkout pages also bind the run’s task before their
limit, which makes corrupt cross-task rows fail closed without starving valid rows.
Transport response-byte limits are propagated into execution storage reads. SurrealDB projects a
conservative materialized-byte estimate before hydrating selected rows, while the in-memory
adapter checks before cloning. execution.diagnose spends one decreasing aggregate byte budget
across its task scope and every nested run/task/delivery lookup rather than granting the full
transport budget to each lookup independently. Some
endpoints diverge further: approval-gate lists allow up to 200 with a default of 50
(mcp/approval-tools.ts:85); the inbox derives its scan window from the requested limit rather
than a flat cap (inbox/service.ts:108-112).