Post

How to deal with 429 in your tests 🧭

How to deal with 429 in your tests 🧭

What happend? 🀨

I stumbled upon a storm of 429 errors while running Playwright tests in CI.

The reason?
The CI worker had a much faster connection than my local machine behind a VPN when writing and debugging tests.

How to handle such inconvenience πŸ‘€

[Solution 1] Add waits in test.afterEach β€” Done πŸ’…

GJ Good Job! (fired)

EXAMPLE
You have 100 tests and add a 500ms wait in afterEach:
100 Γ— 500ms / 1000 = 50 seconds is wasted into oblivion.
  • And this sacrifice of time will only grow exponentially as your test suite scales.

[Solution 2] Use unique API keys or users to avoid hitting the limit

This one is a bit better - but do you really want to bother? πŸ™‚

  • AND what if it’s just one endpoint you’re testing?

[Solution 3] Change system rate limit when CI flag is passed

Technically possible, but again β€” much hassle.
Besides your test become flaky

  • ❗️ YOU need to remember about this
  • ⚠️ YOU change systems original state - not good either
  • 🚫 YOU are fixing symptom, not the cause

One retry to rule them all πŸ’

To fix it cleanly, I added a reusable retry handler for 429 Too Many Requests.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import type { APIResponse } from "@playwright/test";

export async function requestWithRetryOn429(
  reqFn: () => Promise<APIResponse>,
  retries: number = 3,
  backoff: number = 500,
): Promise<APIResponse> {
  const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

  for (let i = 0; i < retries; i++) {
    const response = await reqFn();
    if (response.status() !== 429) return response;
    await delay(backoff * (i + 1));
  }

  throw new Error("Too many retries due to 429 errors");
}

Flowchart for younger generations

flowchart TD
  Start --> CheckRetries
  CheckRetries -->|Retries left| CallRequest
  CallRequest --> CheckResponse
  CheckResponse -->|Not 429| Success[Return response βœ…]
  CheckResponse -->|429| WaitBackoff
  WaitBackoff --> CheckRetries
  CheckRetries -->|No retries left| Fail[Throw error ❌]

Sequence for the OG

sequenceDiagram
    participant Caller
    participant Handler
    participant API

    Caller->Handler: requestWithRetryOn429()
    loop Up to 3 attempts
        Handler->API: reqFn()
        API-->Handler: response
        alt Response is not 429
            Handler-->Caller: return response βœ…
        else Response is 429
            Handler->Handler: wait backoff ⏳
        end
    end
    Handler-->Caller: throw error ❌

Simple exaple of use

1
2
3
4
5
6
7
8
9
...

async function getBalance() {
  return context.get("https://api.ibank.com/my-balance");
}

const response = await requestWithRetryOn429(makeApiCall, 5, 500);

...

Outcome

GJ

  • Tests no longer fail when a 429 Too Many Requests response is received
  • Wait (backoff) is triggered only when a 429 occurs
  • The request is automatically retried after the specified delay
  • CI runs have become significantly more stable
  • This is a reusable solution that addresses the root cause
Check out code repo here:
Playwright example project
This post is licensed under CC BY 4.0 by the author.