The tiny 'slow‑mode' flag I added to our API — and how it stopped a rollout in a Bengaluru office

I added a controllable 'slow mode' to our API to reproduce real‑world latency and packet loss. It caught timeouts our tests missed — and taught me the costs of simulated chaos.

Written by: Arjun Malhotra

Person typing on a laptop with a coffee cup on a wooden desk
Photo by Brooke Cagle on Unsplash

It was 11:30 a.m. and our sales engineer was on a call in a cramped client office in Koramangala. The demo had worked the night before in my home lab on fiber. In that office, every API call timed out at 8–10 seconds. The frontend showed a spinner that never ended. The client was polite but disengaged. My laptop, five hours of debugging, and a dozen console.log inserts later, I still couldn’t reproduce the failure from anywhere inside our office or CI.

We’d written functional tests. We had load tests. None of them modeled a network that loses packets, queasy mobile backhaul, or overloaded NAT gateways that the client’s ISP might have. We were testing happy paths in perfect conditions — the exact conditions our users did not have.

So I built one tiny switch into the API: SLOW_MODE. It was deliberately blunt — a single flag that, when enabled, adds configurable latency, occasional dropped responses, and a small probability of partial responses. It lived behind a feature toggle and an environment variable. I used it locally, in PRs when explicitly requested, and in a controlled staging subset. It isn’t glamorous. It is, however, stupidly effective.

Why a single flag

I wanted something minimal that didn’t require a full network lab, yet could exercise the code paths that timeout or retry. The goals were:

Implementation notes (practical, not theoretical)

I added SLOW_MODE to our request middleware. It accepted a small JSON payload or query params for interactive use, and two ENV variables for automated runs:

For local dev I used a simple deterministic sampler (hash of path + seed) so I could reproduce the same failed request across reloads. In CI/staging I made the sampler random but logged each injection with a unique ID and sent that ID to our tracing system (Zipkin). That made test failures explainable: we could say “this failure was seen with SLOW_MODE id=abc123, here’s the trace.”

Where I ran it

The wins

  1. The Koramangala client: with latency and 10% drop simulation, the frontend’s retry backoff hit an edge-case where two concurrent retries raced and left both requests blocked behind a backpressure limiter we didn’t know existed. Fixing that (switching limiter algorithm and reducing per-client concurrency) made the demo work on a flaky mobile connection.

  2. We discovered user‑visible partial JSON bugs. In production we’d seen occasional UI hangs with no errors logged. SLOW_MODE reproduced the exact partial-body closure that left the JSON parser waiting forever. Fixing the parser to fail fast and show a user‑friendly message prevented multiple support tickets.

  3. Better test coverage for retry semantics. Our unit tests weren’t asserting how many retries the client should do. With SLOW_MODE we added deterministic failure scenarios and asserted both the retry count and the fallback UI.

The honest failures and tradeoffs

SLOW_MODE taught me about shortcuts you pay for.

An everyday constraint that mattered

In India, mobile backhaul and office networks vary wildly. A ₹1,200 dongle, a 4G congested corridor during commute hours, or a municipal building’s shared Wi‑Fi gave me edge cases that no lab emulated. I bought a ₹500 prepaid data SIM and a cheap 4G router to test in the exact conditions that our enterprise clients sometimes use. Combined with SLOW_MODE that was enough to find most problems.

What I walked away with

A controllable, visible chaos switch is more useful than ten theoretical load tests. It forced us to test how our code feels to users — not just whether endpoints return 200s. The second lesson: make the chaos deliberate, observable, and short‑lived. If you can’t log every injected failure with an ID, you’ll create more noise than value.

If you build anything like this, do three things right away: default it off in prod, wire every injection into your tracing system, and require an approval token for any automated runs. After that, the tiny flag will save more demos than it will cost you in accidental flakiness.