Skip to content
IP FediuninFull-Stack & AI Engineer

Live backend

Load and chaos report

The services behind the playground's Live API mode — FastAPI, Django, RabbitMQ, Kafka, workers and a WebSocket hub — under load, and with parts of them broken on purpose. Every number on this page comes from a real run of scripts that are in the repository.

Measured on a developer machine, not in production: the services run in Docker Desktop on Windows with the CPU and memory limits from compose, and the load comes from the same machine. A server will give other absolute numbers; what this shows is the shape — what stays fast, what queues, what breaks and how it comes back.

Send a live request in the playground

Load

Simulated playground visitors, each with a sandbox of their own, doing what the Live API mode does: reads, orders, background jobs, payments and webhooks. Measured on 6 November 2026.

20 visitors

37.4 requests/s · p95 150 ms · 0 failed of 2242

20 visitors
Requestreq/sp50p95p99Failed
GET /api/orders/42Reads · cache7.55890
GET /api/orders/42/checkoutPayments API · Django2.1712300
GET /api/products/7Reads · cache7.64680
GET /api/traces/[id]Traces8.33670
POST /api/ordersOrders · Kafka4.2812130
POST /api/orders (422)Reads · cache2.14690
POST /api/orders/42/payJobs · RabbitMQ21524440
POST /api/reportsJobs · RabbitMQ1.91423290
POST /webhooks/paymentsPayments API · Django1.6811190

Queued work, end to end

From the request's start to the end of its last background step, taken from the trace: measured by the services themselves, not by the load generator.

Queued work, end to end
RequestDonep50p95p99Failed
order → Kafka → 3 groups2511502602700
payment job → worker → provider1201934550
report job → worker1141828320

Busiest containers

Busiest containers
ServiceCPU peak (cores)Memory peak
rabbitmq1.65 CPU145 MB of 512 MB
kafka0.47 CPU396 MB of 768 MB
provider-stub0.19 of 0.25 CPU54 MB of 128 MB
ops-bot0.15 of 0.25 CPU96 MB of 128 MB
orders-api0.09 of 1 CPU76 MB of 256 MB
realtime-hub0.05 of 0.25 CPU54 MB of 128 MB

60 visitors

112.3 requests/s · p95 140 ms · 0 failed of 6737

60 visitors
Requestreq/sp50p95p99Failed
GET /api/orders/42Reads · cache23510130
GET /api/orders/42/checkoutPayments API · Django6.2813430
GET /api/products/7Reads · cache22.948100
GET /api/traces/[id]Traces24.947100
POST /api/ordersOrders · Kafka11.8813160
POST /api/orders (422)Reads · cache6.14780
POST /api/orders/42/payJobs · RabbitMQ6.21625320
POST /api/reportsJobs · RabbitMQ5.91524280
POST /webhooks/paymentsPayments API · Django5.3713380

Queued work, end to end

From the request's start to the end of its last background step, taken from the trace: measured by the services themselves, not by the load generator.

Queued work, end to end
RequestDonep50p95p99Failed
order → Kafka → 3 groups7081402602700
payment job → worker → provider3692129440
report job → worker3581928340

Busiest containers

Busiest containers
ServiceCPU peak (cores)Memory peak
kafka1.56 CPU460 MB of 768 MB
orders-api0.39 of 1 CPU79 MB of 256 MB
rabbitmq0.36 CPU105 MB of 512 MB
ops-bot0.17 of 0.25 CPU96 MB of 128 MB
postgres0.16 CPU100 MB
order-worker0.07 of 0.5 CPU67 MB of 192 MB

Chaos

Each experiment states what should hold, breaks one thing for real — kills a container, stops a broker — measures what happens and whether the system comes back by itself. Measured on 6 November 2026.

A worker dies in the middle of payments

Hypothesis held

What was broken
100 payments whose first provider call fails; while both order-worker replicas work through them, one is killed (SIGKILL)
Hypothesis
No payment is lost and none is charged twice: RabbitMQ redelivers the unacknowledged jobs to the other replica, and the provider gets the task id as its idempotency key.

What was measured

  • Payments finished: 100 of 100 succeeded
  • Charges at the provider: 100 charge ids for 100 payments: none charged twice
  • Redelivered to the other worker: 1 job(s) the killed worker held unacknowledged
  • All done after the kill: 1.1 s

Kafka is down while orders come in

Hypothesis held

What was broken
Kafka stopped; 10 orders created; Kafka started again
Hypothesis
Orders are still accepted at full speed: each event waits in the outbox, in the same transaction as its order, and every consumer group catches up once Kafka is back.

What was measured

  • Orders accepted while Kafka was down: 10 of 10 (201), median 8 ms
  • Events waiting in the outbox: 10
  • Every consumer group caught up after Kafka was back: 16.0 s: 10 reserved, counted and notified

RabbitMQ is down

Hypothesis held

What was broken
RabbitMQ stopped, then started again
Hypothesis
Only queued work is refused, with 503 and Retry-After; reads and orders keep working; once RabbitMQ is back the API and the workers reconnect by themselves.

What was measured

  • A new job while RabbitMQ is down: 503, Retry-After 5 s
  • Everything that needs no queue: reads 200, orders 201
  • Jobs accepted again, without a restart: after 9.1 s
  • That job done by a reconnected worker: 0.0 s later

Redis is down

Hypothesis held

What was broken
Redis stopped, then started again
Hypothesis
The API fails fast and honestly (503 with Retry-After, readiness down) instead of erroring or hanging, and serves again as soon as Redis is back.

What was measured

  • A request while Redis is down: 503, Retry-After 5, in 545 ms
  • Readiness: 503: redis unavailable: TimeoutError
  • Serving again, without a restart: after 2.4 s
  • A sandbox from before the restart: 401: gone with Redis, as designed (nothing personal is kept)
  • Realtime hub subscribed again: after 0.9 s
  • An order right after the restart: its trace completed in 0.5 s: every service reconnected

Found and fixed. The first run disproved this hypothesis: without Redis the Orders API answered 500 after five seconds — nothing handled a lost Redis, and the trace of the failure was written into that same Redis. Now a lost Redis gives a 503 with Retry-After within half a second (the client's timeout), a trace is never written into a Redis that just failed, and a regression test keeps it that way. The load run right after found one more: the first call on a connection opened before the restart failed, so one order event's trace never completed (the order itself was handled). Every Redis client now repeats a call on a closed connection, and this experiment now places an order right after the restart and waits for its trace.

Where it was measured

Machine
AMD Ryzen 5 7500F 6-Core Processor, 12 logical CPUs, 31.6 GB RAM, Windows-11
Docker
Docker Desktop 29.2.1, 12 logical CPUs, 15.4 GB RAM
Load generator
Locust 2.47.0 on the same machine

Container limits (compose)

Container limits (compose)
ServiceReplicasCPUMemory
edge10.5128 MB
kafka1—768 MB
ops-bot10.25128 MB
ops-bot-run10.25128 MB
order-worker20.5192 MB
orders-api11256 MB
outbox-relay10.25128 MB
payments10.5256 MB
payments-sweeper10.1128 MB
postgres1——
provider-stub10.25128 MB
rabbitmq1—512 MB
realtime-hub20.25128 MB
redis1——

Method

  • Load: Locust. Each level ramps up, settles, then is measured over a steady window; a visitor waits 0.2–1 s between actions.
  • The per-visitor rate limits are lifted for the run (compose.load.yaml): one machine plays every visitor from one IP, and the point is the services, not the limiter. The limiter has a scenario of its own in the playground.
  • Latencies are percentiles over the measured window; Locust rounds times above 100 ms to the nearest 10 ms.
  • Chaos: the experiments run one after another; after each, whatever was broken is started again before the next one begins.

Mix of actions per visitor

  • GET /api/orders/42 × 4
  • GET /api/products/7 × 4
  • POST /api/orders (422) × 1
  • POST /api/orders × 2
  • POST /api/reports × 1
  • POST /api/orders/42/pay × 1
  • GET /api/orders/42/checkout × 1
  • POST /webhooks/payments × 1

Run it yourself

cd backend
docker compose up -d --build
uv run --group load python scripts/run_load.py
uv run python scripts/run_chaos.py