Appearance
Synced-store limits
This table describes the synced-store behavior shipped on main. Lengths are JavaScript UTF-16 code units unless a row explicitly says bytes.
| Area | Limit or rule | Value | Options and notes |
|---|---|---|---|
| Store data | Live-data quota | 31,457,280 logical units (30 Mi) per store by default | Counts every live public, private, server-only, system table row as sortKey.length + storedTableName.length + itemKey.length + JSON.stringify(value).length. There is no synthetic + 50. Tombstones, metadata, and file/blob contents do not count. The exact boundary is allowed; a growth past it fails with TotalDatabaseSizeExceededError. An over-limit store may still shrink. The platform configures this quota; apps should prune or split growing collections. Legacy stores are enforced after their exact-accounting backfill completes. |
| Writes | Key length | 2,048 | Measures storedTableName + ":" + itemKey; sortKey is excluded. Private and server-only prefixes count. Enforced by client and server with KeyTooLargeError. maxKeyLength may impose a smaller client limit but cannot raise the server limit. |
| Writes | Serialized value | 1,048,576 | Measures JSON.stringify(value).length; a string can therefore contain at most 1,048,574 characters because its quotes count. Enforced by client and server with ValueTooLargeError. maxValueLength may impose a smaller client limit. A push can contain multiple mutations, so split or separately send values when the whole frame approaches the transport limit. |
| Writes | Distinct rows per app mutation or hook | 10,000 | A row is one (table, itemKey) pair; repeated writes or a sort-key move count once. Deletes may be conservatively counted by the client. Enforced by client and server with TooManyKeysError. Split bulk work across mutations. Platform system mutators and schema migrations are exempt. |
| Writes | Client-side mutations per second | 100 per second sustained | The trusted host currently enforces this as 200 mutations in a fixed 2-second window. This is a temporary ceiling: the supported limit is expected to be lower, and this documentation will be updated when that limit is finalized. Do not design an app to run near the ceiling. If a client exceeds it, the platform unmounts the activity and replaces it with an error UI headed “This activity was stopped.” The UI explains that the app sent too many state mutations, shows a reference ID, and offers a Retry button. |
| Writes | pendingMutationDedupeKey | 256 characters | Client-enforced; shorten the key or omit it. |
| Writes | Mutation atomicity | All or nothing | If a mutator throws or breaches a limit, none of its writes commit. Other mutations in the same push may still commit. Server state is authoritative, so use onFailedMutation and onBackgroundError for failures discovered after the optimistic pass. |
| Keys | Reserved characters | : and null (\0) are forbidden in tableName, itemKey, and sortKey; itemKey must be non-empty | $-prefixed tables are platform-owned and $$-prefixed tables are reserved internals. Choose another key encoding. |
| Values | Supported types | JSON only | Supported: string, number, boolean, null, arrays, and plain objects. Serialize Date, BigInt, Map, Set, ArrayBuffer, class instances, and undefined before writing. |
| Transport | Client-to-server message | 5 MiB of UTF-8 for the entire frame | Breach closes the WebSocket with code 1009. One push can contain multiple mutations plus envelope overhead, so split or separately send writes when the whole frame approaches the limit. |
| Transport | Server-to-client message | 32 MiB Cloudflare ceiling; synced-store targets pull responses below 8 MiB | Large logical sort-key groups are split across numbered response messages, keeping ordinary pull frames comfortably below the deployed transport ceiling. |
| Transport | Data sharing one sortKey | Bounded by the store quota, not by one transport frame | A sort-key group remains atomic to application code. The server may stream it through multiple messages; the client buffers all fragments before applying, persisting, widening its loaded range, or notifying subscribers. A missing or malformed fragment aborts the version seal and retries the pull. Use multiple sort keys for independently pageable data, not to work around frame size. |
| Transport | Protocol error message and stack | 8 KiB each | The head of each field is retained and the rest is truncated. |
| Pulling | Default local pull budget | 100,000 bytes | This is a target, not a hard cap; a whole sort-key group may exceed it. Set initialLocalBudgetBytes to change the initial target. Setting it to 0 loads all data and disables eviction. The client also adjusts the budget at runtime. |
| Pulling | First-render budget | 25,000 bytes by default | Set schema firstRenderBytes to override the target for the first page across all pull windows. A whole sort-key group can exceed it, and always-loaded rows do not consume it. |
| Pulling | Always-loaded rows | No separate row cap; bounded by the total store quota | sortKey: "" is read as one complete logical group. Put growing data behind non-empty sort keys so clients can page and evict it. |
| Pulling | Always-loaded bytes | Exempt from pull budgets; bounded by the total store quota | Always-loaded rows are streamed in bounded transport fragments when needed, but every client still buffers and loads the complete sortKey: "" group on every pull. Keep it small for client memory and bandwidth. |
| Cross-store | External mutation targets per commit | 200 unique (storeTypeId, instanceId) pairs | Multiple mutations to one target count once; a breach rejects the mutation. |
| Cross-store | Direct external mutation depth | 1 | A mutator reached by mutateExternal cannot call mutateExternal again. Indirect action hops share the 10-hop action-depth limit. |
| Cross-store | External read items | 1,000 per read | Results are truncated. Page or narrow the read. |
| Cross-store | External read payload | 1,048,576 bytes per read | Results are truncated. Page or narrow the read. |
| Cross-store | External read timeout | 5 seconds | The read fails on timeout. Keep the target query bounded. |
| Cross-store | Unconfirmed optimistic external | 5 minutes | The client reverts an overlay that remains unconfirmed past the TTL. Dispatch is detached from the source push; retryable infrastructure failures may be retried, so receivers must be idempotent or convergent. Ordinary mutator failures and overload responses are not retried. |
| Cross-store | Watched external stores | 16 per session | Further watch requests are rejected. Unwatch targets that are no longer needed. |
| Cross-store | Pending dispatches | 1,000 waiting per target, plus the active RPC | Overflow fails immediately. Reduce source fan-out or target recovery time. |
| Server execution | Wall clock | 30 seconds per mutator, action, or hook call | The call fails. Bound scans and external work. |
| Server execution | QuickJS memory | 16 MiB configured per call | The call can fail with an out-of-memory error. Prefer bounded scans over materializing a growing table. Some WebAssembly allocations are governed by the hosting memory ceiling rather than this logical limit, so do not treat 16 MiB as a reservation. |
| Server execution | Event-loop ticks | 50,000 per call | The call fails. Bound loops and recursive work. |
| Server execution | Enqueued-action chain | 10 hops | The next hop is rejected. Intermediate commits are not rolled back. |
| Server execution | Concurrent actions | 8 active per store | Additional top-level actions queue. A nested action is rejected immediately when all slots are occupied to avoid deadlock. |
| Server execution | Active plus queued actions | 64 total (8 active and 56 queued) | Further actions fail with queue_full. |
| Server execution | Action queue wait | 30 seconds | The queued action fails with queue_timeout. |
| Server execution | Optimistic-lock commit retry | 3 retries after the first attempt on retry-enabled paths | Lane-routed app pushes do not retry a conflicted commit server-side; the client can safely re-push. Action-invoked mutations and maintenance paths use the bounded retries. |
| Server queueing | Waiting mutation entries | 1,000 | Overflow rejects new queue entries. This is a backpressure bound rather than an app data-model allowance. |
| Server queueing | Mutation groups per drained batch | 300 | Remaining groups drain in a later batch. |
| Server queueing | Buffered presence operations | 4,096 | Further operations are left for a later flush. |
| Identifiers | storeTypeId and instance ID | 1,000 characters each | Registration or routing validation rejects longer identifiers. |
| Storage | Rows per table | Unbounded | Enforce an app-level retention policy and use sort keys for lazy loading. |
| Storage | Tables per store | Unbounded | Prefer a stable, bounded schema; the live-data quota still applies to their rows. |
| Storage | Platform-internal rows per transaction | Unbounded by the 10,000-row app limit | Platform system mutators and schema migrations must establish their own safe bounds. |
| Storage | Pending mutations while offline | Unbounded | Long offline sessions can accumulate indefinitely; keep mutations small and avoid high-frequency writes while disconnected. |
| Storage | Tombstone growth | Self-managing, not capped | Cleanup starts when tombstone logical size exceeds live-data size plus 1,000 units and removes at most 100 tombstones per commit. |
| Storage | Concurrent clients per instance | Unbounded by synced-store | Actual capacity is constrained by the hosting process and workload. |
| Storage | Subscriptions per client | Unbounded | App code should release subscriptions that are no longer needed. |
| Storage | Search index size | Unbounded | Apply an app-level indexing and retention policy. |
| Mutators | Determinism | Required | Mutators run optimistically and authoritatively. Pass timestamps, random IDs, and desired final values as inputs; do not generate them with Date.now(), Math.random(), or toggle expressions. See mutator rules. |
| Mutators | External calls | Not allowed | Use actions for network or platform calls. ctx.serverOnly() and another user's private data are unavailable on the client, so guard server-only access with ctx.isServer. |
| Actions | Client execution | Server-only | Actions have network latency and no optimistic result. ctx.enqueueAction() is a no-op during the client pass. |
| Connections | Duplicate clientId | One active connection | The newer connection kicks the older one with code 4000. Use one stable, unique client ID per tab/session. |
| Connections | Terminal close codes | 4000 kicked; 4001 auth failed; 4002 library mismatch; 4003 store not found | These do not reconnect automatically. Reload or correct the underlying identity/auth/version issue. |
| Connections | Reconnectable close codes | 1000 and 1006 | The client reconnects with backoff. Code 1009 means the outbound client frame was too large; shrink the pending mutation or it can repeat after reconnect. |
| Conflicts | Merge behavior | Last writer wins per row key | There is no field-level merge, CRDT, or custom conflict callback. Use finer-grained rows when independent fields must merge. |
| Client cache | Eviction threshold | More than 1.2 times the local pull budget | The client evicts least-important cached data and refetches it on a later pull. Raise initialLocalBudgetBytes, call the runtime load-more API, or improve pull windows when the working set should stay resident. |