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.
| Variable | Default | Accounting scope |
|---|---|---|
| MAX_CONNECTIONS | 1000 | Live Engine.IO connections plus reserved pending handshakes |
| MAX_INFLIGHT_BYTES | 67108864 | Shared input, queued packets and defensive-copy reserves |
| MAX_ACTIVE_READ_BYTES | 268435456 | Worst-case encrypted rows for active Read/validation |
| MAX_BUFFERED_TOTAL_BYTES | 67108864 | Shared outbound bytes until transport drain |
| MAX_BUFFERED_SOCKET_BYTES | 16777216 | Per-connection outbound bytes |
| MAX_PENDING_COMMANDS_PER_SOCKET | 64 | One decoder's assembly, dispatch, queued and executing packets |
| MAX_COMMANDS_PER_SECOND | 1000 | Per-decoder token-bucket refill rate |
| COMMAND_BURST | 2000 | Token-bucket capacity |
| BINARY_PACKET_TIMEOUT_MS | 30000 | Absolute binary assembly deadline after its header |
| MAX_SUBSCRIPTIONS_TOTAL | 100000 | All socket–ID edges plus reserved slots |
| MAX_SUBSCRIPTIONS_PER_SOCKET | 32768 | One socket's edges plus reserved slots |
| MAX_QUEUED_TOTAL | 32768 | Total Router key reservations; a batch occupies multiple keys |
| MAX_QUEUED_PER_RESOURCE | 100 | Running/waiting reservations for one key |
| MAX_RESOURCE_BYTES | 67108864 | Reassembled ciphertext per extended resource; 1100000–67108864 |
| MAX_TRANSFER_BYTES | 134217728 | Logical operation bytes, including 256 per entry; 1024–134217728 |
| MAX_STAGED_BYTES | 268435456 | Shared staged-write accounting; 1024–4294967296 |
| TRANSFER_TIMEOUT_MS | 60000 | Absolute 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
- 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.
- Before reconstruction, each attachment reserves its length plus 64 bytes. An incomplete packet already consumes shared capacity and a pending slot.
- The same lease follows the reconstructed packet through Socket.IO dispatch. The handler claims it by the body object, not by ACK callback identity.
- 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.
- 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. - 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
| Condition | Result |
|---|---|
| No handshake capacity | Connection rejected; Socket.IO connect_error |
| Shutdown in progress | New handshake/work rejected as not ready |
| Copy/subscription/Router capacity exhausted, or read reservation cannot fit the entire budget | ACK {ok:false,error:"too_large"}; no mutation by that command |
| Read-capacity wait full, expired or cancelled before storage | ACK {ok:false,error:"not_ready"} while the connection remains available; no mutation by that command |
| Parser size/rate/pending/deadline violation | Connection closed; a negative ACK is not guaranteed |
| Output exhausted | Recipient closed; committed data is retained |
| Unexpected failure after mutation starts | Writer 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.