Scope: all three services running in AWS staging (api-staging, face-engine-staging, registry-staging), tested with real HTTP traffic via k6, not synthetic benchmarks or local containers. Staging runs fixed capacity with no autoscaling (see terraform/variables.tf — no aws_appautoscaling_target exists anywhere in the stack), so these numbers are the actual ceiling of the deployed infrastructure as it exists today, not a theoretical estimate.

Methodology

Each service got its own staged ramp — VU (virtual user) count stepped up every 20-45s rather than jumping straight to peak load, so the point where each service starts degrading is visible in the data, not just the final collapse. Stage sizes were chosen per-service based on its actual allocated capacity (see each service’s own load-testing page for the exact stages): Every run was followed by a recovery check (GET /health + aws ecs describe-services) before moving to the next — no service was left in a degraded state.

Summary

The most notable pattern across all three: none of them were CPU- or memory-bound at the point they degraded. CPUUtilization and MemoryUtilization (pulled from CloudWatch for the exact test windows) stayed well under 40% everywhere, even as latency and error rates climbed sharply. This means the current fix for “add more capacity” is not simply “give the ECS tasks more CPU” — the real bottlenecks are elsewhere (see each service’s own page for specifics).

api-staging: full results

Stages: 5 → 20 → 50 → 100 VUs over ~3m15s, mixed GET /health (50%), GET /models (25%), POST /auth/otp/request with a randomized phone per iteration (25%, to avoid the per-phone rate limiter from dominating the results).
Zero failed requests even at 100 concurrent VUs — every single request eventually completed successfully. The cost was tail latency: median stayed a fast 323ms throughout, but p95 climbed to 4.45s and the single worst request took 42.17s. CloudWatch during the peak stage (50→100 VUs): CPU averaged 22%, peaked at 35.5%. Memory peaked at 14.2%. Both api tasks (api_desired_count = 2) stayed up the whole time; no restarts, no OOM kills. Read as: the API absorbs load gracefully rather than shedding it — a real production incident here would look like “everything is slow” rather than “things are failing,” which is a reasonable failure mode but still worth fixing before real traffic, since a 42-second worst case is a timeout in most client SDKs regardless of the eventual 200. The low CPU/memory at peak points at something other than raw compute — likely the size of the Postgres/Redis connection pools, or /models’s synchronous per-request proxy call to face-engine’s /v1/version (2s timeout) becoming a queuing point under concurrent load.

What this doesn’t cover

  • No auth-token-bearing endpoints were load tested (/identity/*, /verify/*, /users/*) — they need a real enrolled user and a valid session per request, which the current script doesn’t set up. The /health//models//auth/otp/request mix tested here exercises the HTTP/routing/DB-connection layer but not the face-comparison or vector-search code paths under load.
  • No sustained (soak) test — every run here is a single ramp-up-and-down over a few minutes, not hours. Whether these services stay stable under sustained moderate load (as opposed to a short spike) is untested.
  • No multi-service concurrent load — each service was tested in isolation, sequentially. Real traffic would hit all three simultaneously (api calling face-engine and registry-simulator on every /identify), which could compound each service’s individual bottleneck.