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 π
- EXAMPLE
- You have
100
tests and add a500ms
wait inafterEach
:
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
- 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.