Skip to main content

Server admission and limits

The default SDK endpoint bounds connections, retained input, concurrent reads, outbound messages, queued work and subscriptions. Work reserves capacity before using the corresponding resources. Read-capacity contention has a bounded FIFO wait; other capacity failures reject admission instead of adding to an unbounded queue.

There is one process, one Router and independent per-ID queues, without a global execution lock. These controls apply to ulda:*; the explicit legacy benchmark endpoint does not inherit the SDK guarantees.

Settings​

Settings are read at process startup. Compose passes only variables explicitly interpolated in compose.yaml; other settings require a Compose environment override. See configuration. Invalid values fail startup. Zero does not disable these limits. MiB means 1048576 bytes.

VariableDefaultAccounting scope
MAX_CONNECTIONS1000Live Engine.IO connections plus reserved pending handshakes
MAX_INFLIGHT_BYTES67108864Shared input, queued packets and defensive-copy reserves
MAX_ACTIVE_READ_BYTES268435456Worst-case encrypted rows for active Read/validation
MAX_BUFFERED_TOTAL_BYTES67108864Shared outbound bytes until transport drain
MAX_BUFFERED_SOCKET_BYTES16777216Per-connection outbound bytes
MAX_PENDING_COMMANDS_PER_SOCKET64One decoder's assembly, dispatch, queued and executing packets
MAX_COMMANDS_PER_SECOND1000Per-decoder token-bucket refill rate
COMMAND_BURST2000Token-bucket capacity
BINARY_PACKET_TIMEOUT_MS30000Absolute binary assembly deadline after its header
MAX_SUBSCRIPTIONS_TOTAL100000All socket–ID edges plus reserved slots
MAX_SUBSCRIPTIONS_PER_SOCKET32768One socket's edges plus reserved slots
MAX_QUEUED_TOTAL32768Total Router key reservations; a batch occupies multiple keys
MAX_QUEUED_PER_RESOURCE100Running/waiting reservations for one key
MAX_RESOURCE_BYTES67108864Reassembled ciphertext per extended resource; 1100000–67108864
MAX_TRANSFER_BYTES134217728Logical operation bytes, including 256 per entry; 1024–134217728
MAX_STAGED_BYTES268435456Shared staged-write accounting; 1024–4294967296
TRANSFER_TIMEOUT_MS60000Absolute lifetime of an uncommitted upload or pinned read; 100–300000 ms

Fixed outbound packet ceilings are 4096 per socket and 65536 overall. Engine.IO handshake and Socket.IO CONNECT waits are bounded at 10 seconds. Wire size limits remain fixed when operational budgets are raised.

MAX_CONNECTIONS=1000 is a configurable admission budget, not a single-process capacity ceiling. The accepted configuration range is 1–100000; that range is not a promise that a machine can sustain that many connections. Compose forwards this variable, so an operator can raise it in .env and recreate the server. Keep memory, subscriptions, pending work and edge limits consistent with the new budget, and measure the intended workload before increasing it in production.

Connection count and updates per second are different measurements. Idle sockets cost memory and heartbeat work; active sockets also consume verification, SQL, serialization and notification capacity. A core-only verify benchmark or an in-memory adapter benchmark does not measure a complete encrypted SDK update persisted in PostgreSQL. The supplied capacity test uses 100 active connections; it does not certify the maximum supported connection count.

Input ownership​

  1. Before header decoding, reserve 1024 bytes per packet plus four times the header's UTF-8 length. These are accounting units, not measured object sizes.
  2. Before reconstruction, each attachment reserves its length plus 64 bytes. An incomplete packet already consumes shared capacity and a pending slot.
  3. The same lease follows the reconstructed packet through Socket.IO dispatch. The handler claims it by the body object, not by ACK callback identity.
  4. Mutation parsing checks all shapes, IDs and logical byte lengths first. Before defensive copies it reserves three times the total logical witness/payload length. Reused attachment placeholders count separately where they create separate copies.
  5. The accepted operation retains ownership through queueing and execution, releasing it once in finally. Rejection, malformed input and reconstruction timeout release only the corresponding unaccepted packets.
  6. Decoder destruction releases incomplete or unclaimed work. Disconnect does not cancel SQL or release the accepted operation's memory reservation early.

The rate bucket consumes one token per Socket.IO header, including CONNECT and DISCONNECT; attachments are not additional tokens. The assembly deadline does not extend when more fragments arrive. Only one root namespace CONNECT is allowed. Unsolicited client ACK packets are rejected because Changed requests no receipt. Engine.IO heartbeats are handled at another layer.

Reads and subscriptions​

Before fetching records for Read or mutation validation, reserve numberOfIds × (1100000 + 167) bytes. The row size is unknown before the query, so small records consume this same conservative reservation.

The default 256 MiB fits at most 243 concurrent record positions. A 64-entry batch reserves 70410688 bytes. One ID needs at least 1100167 bytes; a two-resource Update needs 2200334. A smaller budget can reject even tiny actual records. Commands waiting for Router keys do not hold active-read capacity yet.

After acquiring all Router keys, a command waits up to 5000 ms for its read reservation. At most min(1024, MAX_QUEUED_TOTAL) reservations can wait; existing input, pending-command and Router-key budgets also continue to apply. Waiting commands retain only bounded input and queue metadata, not query results. No storage read starts before capacity is granted. FIFO order prevents smaller new requests from bypassing an older large one. The byte budget is unchanged.

Queue exhaustion or expiry returns not_ready; a reservation larger than the entire configured budget still returns too_large. Disconnect/session close cancels waiting reservations. Shutdown cancels waiters and drains active storage operations. Once SQL starts, disconnect does not release its reservation early. The internal admission snapshot includes waitingReads.

Every read/validation acquires all Router keys before requesting read capacity; a granted reservation never waits for another key or a second reservation. Large-read continuation frames use their pinned snapshot without acquiring either. This ordering prevents a resource-lock/read-budget cycle. A slow or abandoned pinned read can still exhaust other callers' bounded wait; its own absolute transfer deadline and disconnect cleanup remain in force.

PostgreSQL checks each column length and, for ordinary Read, the aggregate witness/payload size before transferring BYTEA values into Node. One statement uses the same MVCC snapshot for checks and bytes, with no metadata/payload race. Any oversized member or aggregate rejects the whole operation without returning payloads. Aggregate accounting includes repeated result slots. The in-memory test adapter checks before copying. Custom findManyByIds(ids,bounds) adapters must honor per-row and optional maxTotalBytes bounds before transfer/copying; a later service check cannot undo an allocation already made by trusted code.

Ordinary Read passes the unchanged 8 MiB aggregate wire ceiling to storage and checks it again before ACK. Validation and pinned large reads keep their own per-row bounds and conservative reservations; the ordinary wire ceiling does not restrict a staged atomic operation. Read capacity is released after the Read ACK or after mutation validation finishes. Output has separate accounting.

Subscription capacity is reserved per connection and globally before awaits or writes. Existing edges are not duplicated; concurrent requests include already reserved slots. Create/Read/Update subscribe before ACK; mixed Delete subscribes updated IDs. Session end removes both indexes and cannot be undone by late work. Deletion removes the ID's edges from the registry. No resource rooms are created.

Extended transfer ownership​

An upload begins with a reservation of count × 256 bytes. Before retaining binary data, it reserves three times the logical witness/payload bytes in the shared staging budget. Each Socket.IO frame also retains its ordinary input lease until its handler finishes. A connection holds at most one upload. Malformed staged data, Cancel, expiry and disconnect release the uncommitted reservation. An accepted Commit detaches it from connection cleanup and keeps it until validation/storage/publication completes, including failure paths.

The upload does not hold database locks or an SQL transaction. Commit obtains all Router keys and validates current rows in groups of at most 64 before one atomic storage call. The count must fit MAX_QUEUED_TOTAL; concurrent work can still make admission fail. These are server budgets, not a catalog schema quota.

A large singleton read pins one ciphertext version and reserves 3 × (MAX_RESOURCE_BYTES + 167) bytes until the last fragment, expiry or close. Validation of an oversized current row uses the same conservative reservation. At the defaults, one such reservation is about 192 MiB; concurrent large reads use the same bounded FIFO admission under the 256 MiB read budget. A small configured read budget can reject an otherwise small resource on this path. No partial snapshot is exposed to the SDK. Ordinary reads retain their existing accounting.

The shared staging snapshot exposes stagedBytes and stagedPeakBytes. These reservations are accounting units, not an exact process-memory ceiling.

Output buffering​

ACKs, Changed, binary attachments and control packets reserve their length plus 64 bytes per frame. Accounting spans the Engine.IO write buffer and the period after flush until transport drain. Flush alone does not release capacity. Upgrade, drain and close release their owned portions once.

If either per-socket or shared output capacity is exceeded, the socket currently receiving that attempted enqueue is closed. This is not a fairness guarantee: a healthy recipient can also be closed when shared capacity is exhausted. Disconnect releases that socket's output and subscriptions, not another socket's reservations. Committed writes stay committed.

Client-visible results​

ConditionResult
No handshake capacityConnection rejected; Socket.IO connect_error
Shutdown in progressNew handshake/work rejected as not ready
Copy/subscription/Router capacity exhausted, or read reservation cannot fit the entire budgetACK {ok:false,error:"too_large"}; no mutation by that command
Read-capacity wait full, expired or cancelled before storageACK {ok:false,error:"not_ready"} while the connection remains available; no mutation by that command
Parser size/rate/pending/deadline violationConnection closed; a negative ACK is not guaranteed
Output exhaustedRecipient closed; committed data is retained
Unexpected failure after mutation startsWriter closed without a negative ACK; unknown result

A sent write may be uncertain after connection loss even if the server actually rejected it before SQL. The client cannot infer definite failure without a reply. There is no automatic mutation replay. Read after reconnect reconciles known IDs; a lost Create ACK cannot recover the generated ID.

Published ulda-auth@1.1.0 maps not_ready to the fixed SERVER_ERROR code. It does not split or retry it. Successful queued reads use the existing ACK format and need no client changes. Only a confirmed too_large read can trigger the SDK's size fallback.

For writes, SDK 1.1.0 can try one staged transfer after a confirmed too_large rejection, including a subscription-capacity refusal. The staged commit applies the same subscription limits before any database write. If it is rejected, the SDK disconnects to discard uncommitted staging; the server releases subscriptions and reservations. Reconnect restores subscriptions through Read without replaying the rejected write. An unknown write outcome does not permit this fallback.

Observation and deployment​

The embedded server.admission.snapshot() exposes numeric current/peak accounting and connection/pending/packet counts. It is not a public HTTP endpoint. Diagnostic logs contain no raw witness, payload, origin, password or database exception. Logging policy.

Budgets do not add up to process RSS. Runtime metadata, garbage collection, JSON parsing, driver copies, database temporary results, proxy/TLS and kernel buffers cost additional memory. Size containers and host headroom from measured payload/batch/concurrency profiles; defaults do not certify any machine capacity.

MAX_CONNECTIONS does not count every raw HTTP/TCP connection before handshake. The rate bucket is per decoder and resets on a new connection, not per IP. Use edge connection/rate controls, TLS, monitoring and backups. One application process does not eliminate native runtime or operating-system threads. A second writer against the same namespace is unsupported.

Lower budgets can reject otherwise valid requests. Tuning must not weaken atomicity, change cryptography or introduce automatic write replay. See deployment and testing.

Large catalogs and combined budgets​

ULDA Auth 1.1.0 removes the fixed 63-content catalog cap. It does not remove server admission limits. On a fresh connection, the default 32,768 subscription ceiling permits at most 32,767 contents plus the master, before other limits. One hundred clients opening 10,000 contents each exceed the global 100,000 subscription budget.

Complete atomic deletion needs Router capacity for every affected ID at commit. Fragmenting the upload does not split that transaction. Staging accounts for three copies of binary bytes plus 256 bytes per item, so an operation below the logical 128 MiB ceiling can still exhaust the 256 MiB staging budget.

Large Changed delivery also uses the outbound queue: a valid resource can exceed the 16 MiB per-connection budget and cause a recipient to disconnect. Reconnect/Read recovers state; Changed has no durable replay log. The SDK eagerly loads all content and re-encrypts the entire master on catalog mutations. Catalog count alone is not a memory or throughput estimate.

Subscriptions use two Maps with per-resource/per-connection arrays. Membership and removal scan the relevant list; the implementation does not scan all clients for each ordinary update. Large fan-out remains proportional to the recipient set. Raising connection or subscription limits requires measurements with the actual payload and fan-out profile.


Reference for Server 1.0.0. Reviewed source.