Skip to content

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.

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.

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.

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.

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).

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.

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 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).