Cameleering

Cameleering / Degraded-fleet test

Cameleering

Cameleering · capacity, recovery and efficiency

How to test your headroom before an incident: the degraded-fleet test

Four tests you can run next week on a staging fleet, in any language and on any stack: capacity with the full fleet and with a third of it gone, a restart under load, a reconnection storm, and overload. What to set up, what to record, how to read it, and how to write pass or fail before you start.

No claim without a number.

Why run it, and what you will learn

Outages often bring no new traffic. They bring the same traffic onto fewer servers, and restarted servers that fall over again when the load reaches them. The headroom essay explains why. This page is the procedure.

At the end you will know four things, each as a number with a date. Where the capacity curve bends (the knee) with N servers and with two thirds of N. How long a restarted node takes to become healthy under load, and whether it stays healthy. What a wave of reconnecting clients costs. And what your service does when it is offered more than it can serve.

Set-up that makes the numbers believable

A capacity number is only as good as the test that produced it. Most of the work is here.

The fleet and the load

  • A staging fleet shaped like production. Same instance type, same ratios between tiers, same configuration, TLS on, the same dependencies (databases, caches, downstream services). It can be smaller, but keep the ratios.
  • Production-shaped load. Replay a sample of real traffic, or model the mix: request types, payload sizes, cache hit rates, and a realistic share of new connections. A single cheap endpoint tells you about that endpoint only.
  • Warm state where production is warm. Fill caches the way production fills them before you measure. Test cold starts separately, on purpose.

The load generator

  • Open-loop, constant rate. Requests are scheduled by the clock, not sent when the previous one returns.
  • On separate machines. Never on the servers under test. Watch the generators' CPU and network; if they saturate, the result is theirs, not yours.
  • Written down. Versions, hardware, kernel, flags, the load script and the date, as the baseline rules ask. A result without them cannot be repeated.

The open-loop point matters most. A closed-loop generator waits for each answer before sending the next request, so when the server slows down, the generator slows down with it and never measures the requests that should have been sent during the stall. Gil Tene called this coordinated omission; it can make the tail look many times better than users see it [1] [2].

Tools that schedule by the clock include wrk2, built to produce a constant throughput load and measure latency from the intended send time [2]; k6 with its constant-arrival-rate executor, an open-model executor that starts iterations regardless of how the system responds [3]; and Vegeta, which attacks at a constant request rate [4]. Check that your tool reports latency from when a request was due, not from when it was finally sent.

Test 1: the degraded fleet

Procedure

  1. Write down the latency target, for example "p99 under 200 ms", and the error budget. Use the target you already promise users.
  2. On the full fleet of N servers, offer load in steps (say 10% of the expected peak per step, 5 minutes each). Record every step.
  3. The highest step where p99 and errors stay within target is the capacity C.
  4. Take a third of the servers out of rotation. Do not change the offered rate. Watch what happens to the survivors.
  5. Repeat the steps on the smaller fleet. Its capacity is C′.
  6. Hold the smaller fleet at today's production peak for as long as your longest past incident.

Pass or fail, written before you run

Write one sentence and commit it next to the test, for example: "With a third of the fleet gone, p99 stays under 200 ms and errors under 0.1% at today's peak load, for four hours."

Written afterwards, a pass line tends to move to where the result landed. Written before, it is a test.

What to record at every step

columnwhy
offered rate and answered rateif they differ, requests are queuing or failing
p50, p99, p99.9 from the load generatorwhat users wait; averages hide the tail
error rate, by kind (timeouts, 5xx, resets)where the failure starts
CPU, memory, open connections, threads per serverwhich resource saturates first
dependency latency and pool waitsthe bottleneck is often a database or a downstream service
load generator CPUproof the generator was not the limit

How to read it

Compare C′/C with the ideal 2/3. Close to 2/3 means the work spreads cleanly. Well below means something shared is the limit: a dependency, a lock, a connection pool sized per fleet rather than per server. Look at the shape too. A sharp knee gives little warning before p99 breaks; a gentle one gives you time to react. Note the step where errors begin, and which resource ran out first.

example, not a measurementfull fleet (12)degraded (8)
capacity at p99 under 200 ms48,000 req/s29,000 req/s
ideal degraded capacity, 2/3 of C–32,000 req/s
C′/C–0.60
first resource to saturateCPUdatabase pool
today's production peak30,000 req/s30,000 req/s

Invented numbers, to show the reading. This fleet looks comfortable at 62% of capacity on a normal day. With a third gone it is past its knee at today's peak, and CPU is not the reason: the database pool is. The pass line fails, and the fix is a pool setting, not more servers.

Test 2: restart under load

Why this test matters more than start-up time alone: cold start is a recovery problem.

With the degraded fleet under steady load, restart one node and let it rejoin. Do it twice: (a) it gets its full share at once, (b) the load balancer ramps it up with slow start.

recordfromto
time to first answerprocess startfirst successful response
time to healthyprocess startpassing the readiness check
time to full speedprocess startthe node's own p99 back within target
flappingdoes it fail its health check again after joining, and how many times

What to look at: warm-up. Caches are empty, connection pools to databases are empty, a JIT runtime has not compiled its hot paths, and the TLS session cache is cold, so every client does a full handshake. The SRE book lists slow start-up and cold caches as a cause of cascading failure [5].

Health-check design decides whether a slow node recovers or flaps. In Kubernetes, a failed liveness probe restarts the container, a failed readiness probe removes the pod from its Services' endpoints, and a startup probe holds both off until the application has started [6]. A liveness probe that fails under load turns a slow node into a restart loop. Readiness should mean "can serve now"; liveness should mean "is stuck and needs a restart".

Slow start exists in the common load balancers, with limits worth knowing:

  • AWS Application Load Balancer: the target group attribute slow_start.duration_seconds ramps a healthy target's requests up linearly; it cannot be used with the least outstanding requests or weighted random algorithms [7].
  • NGINX: the slow_start parameter of server in an upstream recovers weight from zero; it is part of the commercial subscription and cannot be used with hash, ip_hash or random [8].
  • Envoy: slow start mode scales a new endpoint's weight over a window, for round robin, least request and client-side weighted round robin [9].

Test 3: the connection storm

After a failure, clients reconnect together. Drop all client connections at once, for example by restarting the load balancer's listeners or the generators' connection pools, and let clients reconnect. Run it twice: with clients that back off with jitter, and with clients that reconnect at once.

  • Handshakes per second per core, and CPU time per handshake.
  • p99 on connections that were already open during the wave. A storm that starves existing users is worse than one that only delays new ones.
  • Time until latency is normal again.
  • Full and resumed TLS. Run once with session resumption off and once with it on. A full handshake costs more than a resumed one (about a third more in our own test, details and limits), and after a restart the session cache may be empty. The cost of a TLS handshake explains what to measure.

Your load generator must open new connections at the rate you configure, not reuse them. k6 has noConnectionReuse to turn off keep-alive [10]; Vegeta has -keepalive, true by default [4]. Count the handshakes on the server side to confirm.

Test 4: overload

Offer about 120% of C and hold it. Count the answers that came back within the latency target, not only the total. AWS calls the useful part goodput: responses without errors and fast enough for the client to use. A response that arrives after the client gave up is wasted work [11].

  • Does load shedding or admission control engage, and does it refuse work early and cheaply?
  • Do clients retry, and how much? Retries can multiply load. Google's SRE book describes a per-client retry budget of 10% of requests [12]; AWS describes capped retries, exponential backoff with jitter and a local token bucket [13].
  • When the offered load drops back to normal, does the fleet recover by itself? Both the SRE book and AWS advise testing to the breaking point and beyond it [5] [11].

Put it together

One page per run, in the shape of the crossing report. Fill it in and keep it in the repository.

sectionfor this test
Loadthe request mix, share of new connections, rate steps, the generator and how it schedules
Baselinethe build under test and the previous run it is compared with
Terraininstance type, N, the servers removed, load balancer and its settings, dependencies
WaterCPU, memory, connections per server at C and at C′; CPU per handshake
DistanceC, C′, C′/C; restart times; storm recovery time; goodput at 120%
Lossesevery pass line that failed, and the resource that ran out
Repeat itscripts, configuration, raw results
Datewhen it ran

Then turn each pass line into a budget, as step 6 of the playbook describes: a smaller version of test 1 in CI that fails the build when C′ drops or p99 at the target rate rises past the line. Rerun the full set after any change to the hot path, after changes to instance types or dependencies, and before every peak season.

Common mistakes

  • Closed-loop load. The generator slows down with the server, and the tail disappears from the results.
  • A saturated load generator. If its CPU is at 100%, you measured the generator.
  • An empty cache, or a perfect one. Measure the hit rate production has, and test cold starts on purpose.
  • Only the happy fleet size. Capacity with N servers says little about N minus a third.
  • Different builds, unsaid. Compare runs only with the build, flags and date written next to each.
  • Averages. Use p99 and p99.9 from the client side.
  • No pass line beforehand. Then any result can be read as fine.

Our own numbers

Parts of this test we have measured on simoom, our HTTP engine. All of these come from simoom's campaign 4 on DigitalOcean c-4 droplets, 2026-11-12, against 20 other HTTP servers on the same machines (the ledger).

propertysimoombaseline
TLS 1.3 handshakes per second9,396 full, 12,388 resumed1.39x and 1.56x H2O; 24% and 9% behind axum + rustls
overload, 195k req/s offeredabout as many answers in total as the others; 10th of 19 on answers within 1 msactix-web answered 22x more within 1 ms
start-up to first answer0.94 ms, 1st of 21the other 20 servers, same droplets

How we generated the load

The advice above is open-loop. Our own harnesses are not all open-loop, so here is what each figure rests on.

  • Cameleer against Apache Camel (the ledger’s Cameleer rows) is closed-loop. N worker threads each send the next request as soon as the last one is answered, with the same driver on both sides. Throughput is what each side sustains at saturation. The p99 values are latencies at that saturation, timed from when each request was sent, so they are not corrected for coordinated omission. Both sides use the same driver, so the comparison is like for like, but read those p99 figures as relative, not as what an open-loop test at a fixed rate would show (BENCHMARKS).
  • simoom’s request-rate and latency tests are open-loop. They hold a constant rate and time each request from when it was due. The overload test offers a fixed absolute rate over 1,024 connections.
  • simoom’s TLS handshake, HTTP/3 and bulk-download tests are closed-loop. The handshake figures above are handshakes per second at saturation, from 192 workers each opening a new connection per request, not a rate we offered.

These were measured on one droplet type, not on a fleet. We have not yet run the full degraded-fleet test on our own projects. It is the next result we will publish, with the losses, in the ledger.

Common questions

How many servers do I need to test?

Enough that removing a third is meaningful: at least three, and six or more if you can. Keep the production ratios between tiers and dependencies, and remove servers the way they fail in production, for example a whole zone.

Can I run it in production?

Run it in staging first. In production it belongs in a planned game day, within your error budget, with a clear abort rule, people watching, and a way to put the servers back at once. Never start with the overload test in production.

How long does it take?

Plan a day for the first run: set-up and the four tests, plus the hold at peak for as long as your longest incident. Later runs reuse the scripts and take a few hours.

Which load tool should I use?

Any tool that schedules requests by the clock and measures latency from when each request was due, such as wrk2, k6 with the constant-arrival-rate executor, or Vegeta. For the connection storm it must also be able to open new connections at a set rate.

Is this chaos engineering?

It is related. This is a measured capacity test with pass/fail lines written in advance. Chaos tools can do the removing of servers for you; the measuring and the pass lines are what this page adds.

Does it replace capacity planning?

No. It gives capacity planning real numbers: the capacity with a third of the fleet gone, the restart time and the cost of a reconnection wave. Planning still decides how much margin to buy.

Sources

Checked on 2026-11-13.

  1. Gil Tene, "How NOT to Measure Latency", SpringOne2GX 2013 recording.
  2. wrk2, README: constant throughput load and coordinated omission.
  3. Grafana k6, constant-arrival-rate executor.
  4. Vegeta, README: constant request rate and the -keepalive option.
  5. Google, Site Reliability Engineering, chapter 22, "Addressing Cascading Failures".
  6. Kubernetes, liveness, readiness and startup probes.
  7. AWS, Application Load Balancer target group attributes: slow start mode.
  8. NGINX, ngx_http_upstream_module: slow_start.
  9. Envoy, slow start mode.
  10. Grafana k6, options reference: noConnectionReuse.
  11. Amazon Builders' Library, "Using load shedding to avoid overload".
  12. Google, Site Reliability Engineering, chapter 21, "Handling Overload".
  13. Amazon Builders' Library, "Timeouts, retries, and backoff with jitter".