RORK LABJP
EXPO — Expo Agent is out in beta. It runs in the browser and lets you generate and modify apps from prompts while working directly on a project or repositoryNATIVE — What Expo Agent produces are real, shippable native apps for iOS, Android, and the webRN — The React Native team now officially recommends Expo for new projects, citing how much it narrows the decision space AI code generation has to navigateMAX — Rork Max emits native Swift and runs on Claude Code paired with Opus 4.6, while standard Rork generates cross-platform apps with React Native (Expo)CREDIT — Rork is free to start and paid plans begin at $25/month, but credits burn quickly — worth budgeting for before you build seriouslySEPT — App Store submission responses and Android developer verification both take effect in September 2026. Auditing your distribution paths early is the safer moveEXPO — Expo Agent is out in beta. It runs in the browser and lets you generate and modify apps from prompts while working directly on a project or repositoryNATIVE — What Expo Agent produces are real, shippable native apps for iOS, Android, and the webRN — The React Native team now officially recommends Expo for new projects, citing how much it narrows the decision space AI code generation has to navigateMAX — Rork Max emits native Swift and runs on Claude Code paired with Opus 4.6, while standard Rork generates cross-platform apps with React Native (Expo)CREDIT — Rork is free to start and paid plans begin at $25/month, but credits burn quickly — worth budgeting for before you build seriouslySEPT — App Store submission responses and Android developer verification both take effect in September 2026. Auditing your distribution paths early is the safer move
Articles/Dev Tools
Dev Tools/2026-08-08Advanced

The 429s Kept Coming After Recovery: Measuring Retry Jitter Across 2,000 Clients

My backend came back but the 429s did not stop. I rebuilt the scenario in a 2,000-client discrete simulator and measured what plain exponential backoff, delay caps, 10% jitter, and honoring Retry-After actually cost in seconds.

retrybackoffExpo160React Native221reliability

Premium Article

One morning I took down the backend behind my indie developer apps for about four minutes. It was supposed to be a one-line config change; the deploy failed and left the service dead.

I rolled it back and watched the logs, relieved. The relief did not last. The service was up, capacity was back, and the 429s kept coming.

Opening the app on my own phone gave me the same thing. Errors, for several minutes, against a server that was demonstrably healthy.

The cause was in my own retry code. Worse, the part doing the most damage was the part I had written by the book.

I could not reason my way out of it, so I built a small simulator that replays 2,000 clients reconnecting, and measured what each implementation actually costs in seconds.

What was actually happening

When an outage hits, every client with the app open fails at roughly the same instant. Because their failures are aligned, their retries are aligned too.

The moment the server recovers, that aligned population arrives together. Capacity is exceeded, so the server returns 429. The clients that got 429 come back — still aligned.

The server had recovered. My own clients were keeping it busy.

That much is the standard thundering herd story. What I did not have was the quantitative half: which mitigation buys how much, and where the surprises are.

Building the test rig

Experimenting on a live service was not an option, so I wrote a discrete-time simulator. One tick is 100ms, and the loop only models three things: clients sending, the server accepting up to capacity, and rejected clients rescheduling.

The parameters mirror my setup. 2,000 clients, recovery at the 30 second mark, 120 req/sec of post-recovery capacity. Randomness comes from a seeded mulberry32, and every figure below is the mean of five seeds.

// herd.mjs — run with: node herd.mjs
const N = 2000;               // clients that failed together
const RECOVER_MS = 30000;     // when the server comes back
const CAPACITY_PER_SEC = 120; // post-recovery capacity
const TICK = 100;             // one tick = 100ms
const CAP_TICK = CAPACITY_PER_SEC * TICK / 1000;
const BASE = 1000;            // base delay, 1s
 
// Seeded PRNG so results are reproducible
function mulberry32(a) {
  return function () {
    a |= 0; a = a + 0x6D2B79F5 | 0;
    let t = Math.imul(a ^ a >>> 15, 1 | a);
    t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
    return ((t ^ t >>> 14) >>> 0) / 4294967296;
  };
}
 
function run(nextDelay, seed) {
  const rand = mulberry32(seed);
  const next = new Float64Array(N);   // next send time per client
  const att  = new Int32Array(N);     // attempt count
  const prev = new Float64Array(N);   // previous delay (for decorrelated jitter)
  const done = new Uint8Array(N);
  for (let i = 0; i < N; i++) { next[i] = rand() * 2000; prev[i] = BASE; }
 
  let sentAfterRecovery = 0, ok = 0, peak = 0, t99 = -1;
 
  for (let t = 0; t < 1200000; t += TICK) {
    const arrivals = [];
    for (let i = 0; i < N; i++) if (!done[i] && next[i] <= t) arrivals.push(i);
 
    const up = t >= RECOVER_MS;
    if (up) {
      sentAfterRecovery += arrivals.length;
      const qps = arrivals.length * 1000 / TICK;
      if (qps > peak) peak = qps;
    }
 
    // Everything fails before recovery; afterwards, anything over capacity gets a 429
    let budget = up ? CAP_TICK : 0;
    for (const i of arrivals) {
      if (budget >= 1) { budget--; done[i] = 1; ok++; }
      else {
        att[i]++;
        const d = nextDelay(att[i], prev[i], rand);
        prev[i] = d;
        next[i] = t + d;
      }
    }
    if (t99 < 0 && ok >= N * 0.99) t99 = t;
    if (ok === N) break;
  }
  return { sentAfterRecovery, ok, peak: Math.round(peak), t99 };
}

I track three numbers: total requests reaching the server after recovery, peak post-recovery QPS, and the moment 99% of clients have gotten through.

Mean latency would be misleading here. What people experience during an outage is when the slowest slice comes back.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
Measured numbers showing exponential backoff without jitter finishing 7x slower than a naive fixed 5s interval
Why the implementation that honors Retry-After exactly was the slowest of all, and how to rewrite it as a floor
A drop-in fetch wrapper combining decorrelated jitter with a per-install offset, with smoke test output
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

Dev Tools2026-08-04
Exactly 20 Characters, Still Rejected: Putting Character Counting Behind One Boundary
Emoji and combining marks make your client and your server disagree about length. I measured four counting methods on Node v22 and found 75.6% of naive truncations split a grapheme and 36.1% produced replacement characters over UTF-8. Here is the shared-module design that fixed it.
Dev Tools2026-07-30
What Renovate may bump in an Expo app, and what it must never touch
Turning on automated dependency updates in a Rork-generated app also hands Renovate the 123 packages Expo SDK 57 pins. Measured on 2026-07-30, six of them sit a full major version behind npm latest. Here is how to generate the ignore list from the SDK instead of maintaining it by hand.
Dev Tools2026-07-28
Counting what prebuild --clean will erase before you upgrade to Expo SDK 57
A raw diff between two generated ios/ trees showed 649 changed lines; only 3 were real edits. How to count what prebuild --clean erases, and move it into a config plugin.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →