Skip to content

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.

AreaLimit or ruleValueOptions and notes
Store dataLive-data quota31,457,280 logical units (30 Mi) per store by defaultCounts 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.
WritesKey length2,048Measures 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.
WritesSerialized value1,048,576Measures 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.
WritesDistinct rows per app mutation or hook10,000A 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.
WritesClient-side mutations per second100 per second sustainedThe 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.
WritespendingMutationDedupeKey256 charactersClient-enforced; shorten the key or omit it.
WritesMutation atomicityAll or nothingIf 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.
KeysReserved 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.
ValuesSupported typesJSON onlySupported: string, number, boolean, null, arrays, and plain objects. Serialize Date, BigInt, Map, Set, ArrayBuffer, class instances, and undefined before writing.
TransportClient-to-server message5 MiB of UTF-8 for the entire frameBreach 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.
TransportServer-to-client message32 MiB Cloudflare ceiling; synced-store targets pull responses below 8 MiBLarge logical sort-key groups are split across numbered response messages, keeping ordinary pull frames comfortably below the deployed transport ceiling.
TransportData sharing one sortKeyBounded by the store quota, not by one transport frameA 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.
TransportProtocol error message and stack8 KiB eachThe head of each field is retained and the rest is truncated.
PullingDefault local pull budget100,000 bytesThis 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.
PullingFirst-render budget25,000 bytes by defaultSet 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.
PullingAlways-loaded rowsNo separate row cap; bounded by the total store quotasortKey: "" is read as one complete logical group. Put growing data behind non-empty sort keys so clients can page and evict it.
PullingAlways-loaded bytesExempt from pull budgets; bounded by the total store quotaAlways-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-storeExternal mutation targets per commit200 unique (storeTypeId, instanceId) pairsMultiple mutations to one target count once; a breach rejects the mutation.
Cross-storeDirect external mutation depth1A mutator reached by mutateExternal cannot call mutateExternal again. Indirect action hops share the 10-hop action-depth limit.
Cross-storeExternal read items1,000 per readResults are truncated. Page or narrow the read.
Cross-storeExternal read payload1,048,576 bytes per readResults are truncated. Page or narrow the read.
Cross-storeExternal read timeout5 secondsThe read fails on timeout. Keep the target query bounded.
Cross-storeUnconfirmed optimistic external5 minutesThe 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-storeWatched external stores16 per sessionFurther watch requests are rejected. Unwatch targets that are no longer needed.
Cross-storePending dispatches1,000 waiting per target, plus the active RPCOverflow fails immediately. Reduce source fan-out or target recovery time.
Server executionWall clock30 seconds per mutator, action, or hook callThe call fails. Bound scans and external work.
Server executionQuickJS memory16 MiB configured per callThe 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 executionEvent-loop ticks50,000 per callThe call fails. Bound loops and recursive work.
Server executionEnqueued-action chain10 hopsThe next hop is rejected. Intermediate commits are not rolled back.
Server executionConcurrent actions8 active per storeAdditional top-level actions queue. A nested action is rejected immediately when all slots are occupied to avoid deadlock.
Server executionActive plus queued actions64 total (8 active and 56 queued)Further actions fail with queue_full.
Server executionAction queue wait30 secondsThe queued action fails with queue_timeout.
Server executionOptimistic-lock commit retry3 retries after the first attempt on retry-enabled pathsLane-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 queueingWaiting mutation entries1,000Overflow rejects new queue entries. This is a backpressure bound rather than an app data-model allowance.
Server queueingMutation groups per drained batch300Remaining groups drain in a later batch.
Server queueingBuffered presence operations4,096Further operations are left for a later flush.
IdentifiersstoreTypeId and instance ID1,000 characters eachRegistration or routing validation rejects longer identifiers.
StorageRows per tableUnboundedEnforce an app-level retention policy and use sort keys for lazy loading.
StorageTables per storeUnboundedPrefer a stable, bounded schema; the live-data quota still applies to their rows.
StoragePlatform-internal rows per transactionUnbounded by the 10,000-row app limitPlatform system mutators and schema migrations must establish their own safe bounds.
StoragePending mutations while offlineUnboundedLong offline sessions can accumulate indefinitely; keep mutations small and avoid high-frequency writes while disconnected.
StorageTombstone growthSelf-managing, not cappedCleanup starts when tombstone logical size exceeds live-data size plus 1,000 units and removes at most 100 tombstones per commit.
StorageConcurrent clients per instanceUnbounded by synced-storeActual capacity is constrained by the hosting process and workload.
StorageSubscriptions per clientUnboundedApp code should release subscriptions that are no longer needed.
StorageSearch index sizeUnboundedApply an app-level indexing and retention policy.
MutatorsDeterminismRequiredMutators 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.
MutatorsExternal callsNot allowedUse 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.
ActionsClient executionServer-onlyActions have network latency and no optimistic result. ctx.enqueueAction() is a no-op during the client pass.
ConnectionsDuplicate clientIdOne active connectionThe newer connection kicks the older one with code 4000. Use one stable, unique client ID per tab/session.
ConnectionsTerminal close codes4000 kicked; 4001 auth failed; 4002 library mismatch; 4003 store not foundThese do not reconnect automatically. Reload or correct the underlying identity/auth/version issue.
ConnectionsReconnectable close codes1000 and 1006The client reconnects with backoff. Code 1009 means the outbound client frame was too large; shrink the pending mutation or it can repeat after reconnect.
ConflictsMerge behaviorLast writer wins per row keyThere is no field-level merge, CRDT, or custom conflict callback. Use finer-grained rows when independent fields must merge.
Client cacheEviction thresholdMore than 1.2 times the local pull budgetThe 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.