Stress Test Report
Phase 3 of the Beta Launch production-readiness pass — local, synthetic load testing of the Copy Engine at 100 / 500 / 1,000 / 5,000 / 10,000 simulated concurrent users. Directly closes the gap docs/LIVE_COPY_ENGINE.md already flagged plainly: *"No sub-50ms/ sub-250ms performance validation was performed, and could not be, in this environment. There is no live broker connectivity, no production-scale traffic, and no load-testing infrastructure available here."* That's still true of live broker connectivity and real production traffic — this report doesn't change that — but it does replace "not benchmarked at all" with real, reproducible, local numbers for the one thing that actually can be measured honestly in this environment: the Copy Engine's own CPU-bound processing cost.
Why this is scoped the way it is (read this before the numbers)
This environment has:
- No production infrastructure — no deployed server, no
load-testing cluster.
- No broker connectivity — every connector's own live-vs-simulated
status is already documented in docs/BROKER_INTEGRATION_REPORT.md; nothing here changes that.
- A real, live-linked Supabase project (configured in
.env.local
for local development) — but it is a shared dev resource, not something to point thousands of synthetic concurrent requests at without its owner's own separate, explicit go-ahead. Real database round-trip latency and connection-pool behavior under load are therefore not measured here, and that is the single biggest gap in this report — see "What this does NOT tell you" below.
Given that, this report measures the one thing that's both real and safe to measure locally: `CopyEngine.submit()`, the exact class every live trade copy runs through, driven against `FakeCopyPersistence` — the same in-memory, zero-I/O fake every existing Copy Engine test in this codebase already uses (tests/helpers/copy-engine-fakes.ts). This is real production code under real synthetic load, not a toy reimplementation — it just replaces the database and broker connector at the boundary with an in-memory stand-in, the same substitution this codebase's entire test suite already relies on for correctness testing.
Script: scripts/stress-test-copy-engine.ts (npm run stress-test).
Test machine
Measured on the local development machine this pass ran on — not production server hardware, and on the weaker end of what a real deployment target would use:
- CPU: Intel N95, 4 cores (a low-power mobile/mini-PC part)
- RAM: 16.9 GB
- OS: Windows 10.0.26200
- Node: v24.19.0
A real production VM (even a modest cloud instance) would very likely outperform these numbers — treat them as a conservative floor, not a ceiling.
Methodology
"N users" = N distinct master trading accounts, each with one active copy relationship to one distinct follower account, each firing exactly one 'open' trade signal simultaneously (Promise.all) through one shared CopyEngine instance. This is the real worst-case burst shape — TradeEventQueue (src/lib/copy-engine/reliability/event-queue.ts) gives each master account its own FIFO lane, and different masters' lanes run concurrently with each other — so N different users' masters all trading in the same second (a plausible real scenario: a major news release moving many traders' EAs at once) is exactly what this simulates. RiskCalculationService's pure filter/risk functions are also benchmarked in isolation (zero I/O, no queue, no persistence) as a CPU-only baseline.
Each level measures: total wall-clock time, throughput (events/sec), per-event latency distribution (p50/p95/p99/max — timed individually per signal, not just the batch total), heap growth (process. memoryUsage()), and CPU time (process.cpuUsage()).
Results
Pure risk-calculation baseline (no queue, no persistence, no async — checkTradeFilters + checkAccountRisk + computeVolume per iteration):
> 1,000,000 iterations in ~29ms — ~34 million iterations/sec
The actual risk/filter math is not the bottleneck at any scale tested — this confirms the numbers below are dominated by queueing, async overhead, and per-event bookkeeping, not the business-rule computation itself.
CopyEngine.submit() end-to-end (representative run — see "Run it yourself" below for exact reproduction):
| Users | Total time | Throughput | p50 latency | p95 latency | p99 latency | Heap growth | Success rate |
|---|---|---|---|---|---|---|---|
| 100 | 12ms | 8,588 events/sec | 7.8ms | 9.2ms | 11.4ms | 3.4 MB | 100/100 |
| 500 | 41ms | 12,227 events/sec | 29.9ms | 39.9ms | 40.4ms | 7.6 MB | 500/500 |
| 1,000 | 63ms | 15,893 events/sec | 45.3ms | 61.2ms | 62.0ms | 12.5 MB | 1,000/1,000 |
| 5,000 | 555ms | 9,014 events/sec | 435.3ms | 541.8ms | 549.3ms | 65.6 MB | 5,000/5,000 |
| 10,000 | 1,777ms | 5,627 events/sec | 1,452.3ms | 1,760.2ms | 1,770.6ms | 66.1 MB | 10,000/10,000 |
Zero failures at every level — every signal was correctly processed (risk-checked, filtered, "copied") regardless of burst size; nothing here is about correctness under load, only speed.
The one real bottleneck this found
Throughput peaks around 1,000 simultaneous in-flight events (~16k/sec) and then degrades at 5,000 (~9k/sec) and 10,000 (~5.6k/sec por event). p50 per-event latency grows roughly with N (7.8ms → 1,452ms from 100 to 10,000 users) — this is the expected, correct signature of a single-threaded process working through a burst: the queue's per-master lanes provide *ordering* and *isolation* (one slow/failing master never blocks another's lane), not *parallelism* beyond what one CPU core can execute. Node.js runs JavaScript on one thread; 10,000 simultaneous lanes still execute their synchronous work one at a time, interleaved at await points. The last event in a 10,000-wide burst necessarily waits for roughly its share of the other 9,999 to get their turn.
What this means for production: one CopyWorker process (docs/LIVE_COPY_ENGINE.md) is not going to get meaningfully faster under burst load by any in-process change — the actual lever is horizontal scaling: running multiple CopyWorker processes, each owning a shard of users (e.g. by user id hash or organization id), so a 10,000-user burst becomes several ~1,000-2,500-user bursts running in true parallel across processes/cores instead of one 10,000-user burst serialized on one core. docs/LIVE_COPY_ENGINE.md already anticipated this as a "deployment decision outside this codebase's current scope"; this report gives it a concrete number to plan sharding around: keep each `CopyWorker` process's simultaneous burst size under ~1,000 users worth of masters to stay in the flat, low-latency part of this curve.
Heap growth is roughly linear with N (3.4 MB → 66 MB from 100 to 10,000 users) and never spiked or plateaued unexpectedly — no evidence of a memory leak in this pipeline at these levels, though a single burst-and- discard run like this can't rule out slow leaks that only show up over sustained, repeated load (a longer soak test would be the natural follow-up, not built here — see "What this does NOT tell you").
What this DOES tell you
- The Copy Engine's own risk/filter/queue logic is fast and correctly
isolated per master account — confirmed, not assumed.
- The real, current single-process throughput ceiling for a burst of
simultaneous trade signals, on modest hardware: roughly 1,000-2,000 truly-simultaneous events before per-event latency starts climbing past ~50-100ms, and roughly 5,000+ before it's climbing past several hundred ms.
- A concrete, numeric basis for a horizontal-sharding plan, instead of
the prior "architecture-consistent, not benchmark-verified" caveat.
What this does NOT tell you (read before trusting these numbers for capacity planning)
- Real database latency.
FakeCopyPersistencehas zero I/O — every
real deployment write goes through Postgres (via Supabase), which has real network round-trip time, connection pool limits, and lock contention this report cannot see. This is the single biggest gap: real per-event latency in production will be higher than every number above, by an amount this environment cannot measure. src/lib/ supabase/service-role.ts's own documented pattern — a fresh Supabase client constructed per call, since this app has no persistent process outside CopyWorker — means connection-pool exhaustion under a real burst is a real, untested risk worth its own follow-up pass once a real (or realistic staging) database is available to test against.
- Real broker latency. Every connector's own order-placement/
position-sync latency is a real network round trip to a real broker server (or MetaApi, for MT5) — not modeled here at all.
- Multi-process/horizontal behavior. This ran one Node process.
Real gains from sharding across multiple CopyWorker processes are reasoned about above, not measured — no multi-process harness was built this pass.
- Sustained load / soak testing. This is a burst-and-measure-once
test, not a sustained-throughput-over-time test. Slow memory leaks, GC pause accumulation, or degradation over hours of continuous operation are not covered.
- The web app's own HTTP layer (Next.js request handling, API
routes, page rendering under concurrent user traffic) is not measured here at all — this report is scoped to the Copy Engine specifically, the component the launch checklist's "copy latency" and "queue performance" items are actually asking about.
Run it yourself
npm run stress-testPrints a human-readable summary per level plus a JSON blob (the exact shape the table above was built from) for anyone who wants to compare a different machine's numbers or re-run after a future Copy Engine change.