Table capacity and performance
Choose an access path by the work it does: local mapped records, HTTP queries or persistent table synchronization. Compare latency, payload rates, working-set size and contention together.
The evidence below covers local synthetic benchmarks and passive production HTTP observations. Maximum capacity has not been established for any mode. WebSocket and TCP table synchronization have no published capacity measurement here. The implementations describe capabilities, not measured speed or a ranking between transports.
| Access mode | Strength | Limits and evidence |
|---|---|---|
| Local memmap | Processes on one host share mapped record pages. Resident data can be accessed without an HTTP round trip or JSON serialization. | Warm-cache table reads and writes are benchmarked below. The working set, page faults, storage and the per-table index mutex affect capacity. Remote clients cannot share these local pages. |
| HTTP Data API | Authenticated queries resolve entities, select data and return JSON with provenance to remote clients. | Observed means and payload sizes are available. Query work, serialization and network delivery add costs; the server timer excludes delivery. Concurrent-client capacity is unmeasured. |
| WebSocket table sync | A persistent connection transfers LZ4-compressed binary record batches to another table, avoiding JSON conversion of each row. | Throughput and end-to-end update lag are unmeasured. Lookback scanning, polling, compression, pacing, client fan-out and receiver apply work affect capacity. |
| TCP socket table sync | A persistent TCP connection transfers length-prefixed, LZ4-compressed binary record batches to another table. | Throughput and end-to-end update lag are unmeasured. It shares the synchronization and pacing logic with WebSocket table sync; lower framing overhead alone does not establish higher capacity. |
For both table-sync transports, the client APIs default to a pacing target of 1,000,000 compressed body bytes per second per connection, about 0.954 MiB/s. This is a configurable target, not a benchmark or a hardware limit; framing and network overhead are additional. Raw record throughput can exceed the compressed byte rate when data compresses well. Table synchronization tracks record state within its lookback settings; it does not establish delivery of every intermediate update.
Local memmap: measured read and write costs
Two synthetic runs on 31 October 2026 used SharedData 6.86.0, one process and warm page-cache data. Creation, JIT compilation and warm-up preceded timing. The dataset contained 5,000 keyed records and 10,000 history records; the range selection returned 125 rows. These measure the Python table API, including indexed selection, rather than a raw memory load.
| Operation | Rows per call | p50 µs, runs 1 / 2 | p95 µs, runs 1 / 2 |
|---|---|---|---|
| Primary-key read | 1 | 46.89 / 47.72 | 77.28 / 72.61 |
| Date range for one symbol | 125 | 195.56 / 203.08 | 314.58 / 261.61 |
| Upsert one record | 1 | 79.87 / 82.22 | 122.17 / 121.41 |
| Batch upsert, mmap-visible | 100 | 225.38 / 191.18 | 296.95 / 250.55 |
| Batch upsert plus explicit write() | 100 | 6,132.67 / 6,060.99 | 6,328.46 / 7,277.40 |
Small warm reads and batched updates are strengths in this workload.
The explicit table write() step costs substantially more
than making an update visible through mmap. Neither variant establishes
fsync-level durability. These single-process latency samples do not
establish maximum operations per second, RAM bandwidth, cold-cache
performance or capacity under concurrent readers and writers.
Read the benchmark method and sample counts, or download the sanitized reports for run 1 and run 2.
HTTP API: observed production latency and payload work
Observed server timings and response volume from normal production traffic, collected without issuing data queries or a load test. A separate observation measures payload volume over elapsed wall time.
Measured runtime: 6.86.0. Counter snapshots were collected on 4 November 2026, 14:08:06–14:08:53 UTC. Results cover seven of eight workers alive during collection; the bounded observation ended before the remaining worker was reached. These are partial observations of one deployment and its workload, with no service-level guarantee.
Each worker's counters cover completed requests since that worker started. The oldest observed history starts at 13:20:57 UTC that day; other workers started later. The collection interval above describes when the counters were gathered.
| Operation | Requests | Mean server ms | Mean response KiB | HTTP errors |
|---|---|---|---|---|
Source discovery/api/data/sources |
24 | 74.20 | 9.3 | 0 |
Symbol listing/api/data/symbols |
3 (small sample) | 98.65 | 457.0 | 0 |
Symbol identity/api/data/symbol/{symbol} |
24 | 4.09 | 5.9 | 0 |
Market-data fetch/api/data/marketdata/{symbol} |
1,706 | 584.38 | 1,930.1 | 2 |
KiB = 1,024 bytes. HTTP errors count responses with status 400 or higher. The market-data observations contain 1,704 HTTP 200s and two HTTP 500s. The three-request symbol-listing history is too small for a stable estimate.
Request sizes and query shapes differ between operations. A market-data response averaged about 1.9 MiB, so its mean should not be used as the latency of a small record lookup. The counters contain no date-range, row-count, field-count or cache-state breakdown. No measurements are claimed for the other Data API routes.
Payload size puts that latency in context: the exact market-data means give 3.23 MiB per server-timed second, from 1,976,438.26 response bytes divided by 0.58437594 seconds and 1,048,576 bytes per MiB. This equals total counted response bytes divided by total recorded server duration for those requests. It is a payload/server-time ratio, not a mean of individual request transfer rates. It excludes network delivery and does not establish maximum throughput or the number of clients that can be served concurrently.
What the numbers measure
The running server's /api/traffic_stats counters provide a
count and mean for each operation in each worker. The observer identified
responding workers through local socket ownership, retained one latest
snapshot per worker, and combined the means using request counts as weights:
mean_ms = 1000 × sum(worker_requests × worker_mean_seconds)
/ sum(worker_requests)
The timer runs inside Flask, from its request timing hook to its response hook. Network round trips, proxies, body transfer to the client and work before the timing hook are outside this measurement. Initial authentication checks can precede that hook. Response sizes are the bytes counted by the application before any downstream proxy compression.
p50, p95, p99 and maxima are unavailable. The live operation counters do not retain individual latencies or histograms, and no matching per-request Data API timing records were available. Percentiles cannot be reconstructed from counts and means.
Unfinished requests, including requests still blocked on a lock, are absent from completed-request counters. Retired workers' histories and the unobserved worker are also absent. These omissions can understate the cost of contention. The values mix the covered workers' warm-up and later work; cold-cache latency, maximum throughput and saturation were not isolated.
Bandwidth: observed response payload throughput
A separate passive observation on 4 November 2026, 14:36:36–14:38:10 UTC covered 94.02 seconds of normal traffic on runtime 6.86.0. Matched counter changes account for 40,089,055 response bytes across 105 completed Data API responses, all HTTP 200. Dividing those bytes by the full observation interval gives 3.41 Mbit/s (0.407 MiB/s) of observed application payload generation. This is a conservative partial-coverage rate.
| Operation | Completed responses | Response bytes |
|---|---|---|
Market-data fetch/api/data/marketdata/{symbol} |
97 | 38,938,952 |
Source discovery/api/data/sources |
3 | 28,446 |
Symbol identity/api/data/symbol/{symbol} |
3 | 18,076 |
Symbol listing/api/data/symbols |
2 | 1,103,581 |
| Counted total | 105 | 40,089,055 |
Mbit/s = 1,000,000 bits per second; MiB/s = 1,048,576 bytes per second. Rates and elapsed time are rounded. This window is separate from the lifetime latency counters above.
Six of eight live workers were reached; five had at least two snapshots and contributed counter deltas. Each contributing worker's first and last snapshots span a different subinterval, ranging from 10.02 to 88.01 seconds. Traffic before its first or after its last snapshot, the worker seen only once and the two unobserved workers are omitted. The denominator remains the entire 94.02-second window, rather than adding rates from unequal windows or extrapolating missing traffic.
counted_bytes = sum(last_response_bytes - first_response_bytes
for each matched worker)
observed_Mbit_per_second = 8 × counted_bytes / wall_seconds / 1000000
This does not measure maximum bandwidth or client download speed.
The server counts response Content-Length at its response
hook, before delivery. These bytes describe generated application bodies;
they do not confirm that clients received them. Headers, TCP/TLS overhead,
retransmissions, downstream compression and client connection speed are
outside these counters. The observed request-body byte change was zero;
that does not establish zero incoming network traffic or upload capacity.
The observer made 48 sequential metadata reads, at least two seconds apart, with the same 250 ms stop threshold and no retries. Observed metadata round trips ranged from 14.4 to 27.7 ms. No data queries, concurrent downloads or saturation traffic were injected. Worker identities remained stable, and health, runtime version and build matched before and after collection. Maximum transfer capacity and sustained throughput under a controlled workload remain unmeasured.
What a capacity decision still needs
Storage size, response rate and concurrent-client capacity are different measurements. A useful sizing result fixes the workload and reports what happens as demand increases. The measurements on this page provide starting points; none is a saturation test.
- Record footprint and working set. Live record bytes
are
row_count × dtype.itemsize. Reserved table capacity, headers and index files add storage. RAM demand depends on resident data and index pages, plus process buffers and conversion objects. A mapped table can exceed RAM; accesses to nonresident pages incur page faults and storage I/O. - Comparable payloads. Fix row width, row count, selected fields, date range, access pattern and batch size. Report both rows/s and logical MiB/s. For remote access, also measure delivered bytes/s at the client and identify JSON or binary encoding, compression, framing and TLS. Compressed wire bytes and raw record bytes are different quantities.
- Readers, writers and fan-out. Measure one busy table and multiple tables separately, with the intended read/write mix. Track operations/s, errors, CPU, memory and p50/p95/p99 latency as clients increase. Each synchronization subscriber adds selection, transfer and receiver work. Track initial catch-up separately from steady-state update lag and verify receiver values.
- Cache and persistence conditions. Separate warm resident data from cold reads, setup and index growth. Time explicit persistence separately from mmap visibility. More processes or a different transport still share the same underlying table constraints.
A controlled comparison needs an isolated table and bounded client workload. Production observations here added no table queries or load tests. WebSocket/TCP capacity, cold-cache behavior, contention tails and maximum sustainable rates remain open measurements.
How production load was kept bounded
Both production observations issued zero Data API or table-query requests. For the latency observation, the observer read existing counters sequentially: two validation reads, then a run capped at 48 metadata probes, spaced at least one second apart. Each probe had a one-second connection timeout and a two-second read timeout; the run would stop after an error or a metadata round trip exceeding 250 ms. There were no automatic retries.
Only route-level counts, timings, sizes and status totals were used for this page. Instruments, sources, dataset addresses, users, tokens, machine identities and production record values are omitted. No services, configuration, indexes or datasets were changed for the measurement.
A client timeout alone does not cancel work already running on the server. That is why this observation used existing counters instead of injecting table reads or lock contention.
Storage and concurrency limits
- One exclusive process-shared mutex per table protects the index. Indexed lookups and upserts serialize on that table's mutex. Each table has its own lock. Reading existing mapped record bytes is a separate operation; it does not provide an atomic multi-record snapshot.
- More API workers do not parallelize one table's index. Multiple request processes and writers still contend for the same mutex. Extra threads can overlap socket I/O while one busy table remains a bottleneck.
- Structural work can cause long waits. Opening a table, rebuilding an index or growing capacity can hold its lock for much longer than an ordinary lookup. A regular table lock has no general per-request timeout. The special SCHEMA timeout does not bound every table query, and a Gunicorn timeout is not a reliable individual request deadline for a threaded worker.
- Waiter caps are admission control. Where supported and configured, a per-table cap can reject excess waiting requests with HTTP 503. It does not time-bound requests already admitted. This observation did not stress-test overload protection.
- A row limit does not bound all query work. Resolution, index traversal, filtering and sorting can happen before the returned rows are limited. Narrow entity, date and field selections help bound the workload; scans, large payloads and cold catalog work cost more.
- Shared pages are local to one host. HTTP answers copy and serialize data. Page faults, storage and network costs remain; visibility in memory and persistence on disk are separate guarantees. These observations measure neither durability nor ingestion delay.
Read the Data API contract and table interfaces. The separate socket and WebSocket connection guide describes setup, authentication, synchronization and transport limits. The synthetic local-container benchmark reports p50/p95 for a different, isolated Python workload.