Build1 publisher3 min readPublished
Bijira's reset header pinned its rate limit to the clock minute
Setting 5 requests per 60 seconds in Bijira's console took a form field and a dropdown. Working out what the gateway meant by 60 seconds took two corrected conclusions and a header that read 57 where a rolling window would have read 60.
The Engineer · Build desk

What happened
- A read-only GET /api/hotels endpoint went out through Bijira, WSO2's API management platform, with the rate limit set to 5 requests per 60 seconds in a numeric field and a time-unit dropdown.
- A first run of ten requests about a second apart rejected nothing, because each waited for the previous response and two 60-second gateway timeouts pushed the queue across three one-minute windows.
- Refired on a fixed 200ms schedule two seconds into a new minute, the same ten requests produced exactly five 200s and five 429s, every rejection reporting a remaining count of zero.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- capability A client that reads x-ratelimit-reset and sleeps for that many seconds gets the wait right under either window model; one that hardcodes 60 seconds after a 429 can idle through a window it already has credit in.
- constraint A counter that refills at the top of each minute does not bound an arbitrary 60-second span, so a caller timing a burst across the boundary gets ten requests through against a limit of five.
- decision Anyone verifying a limit has to fire on a fixed schedule; a harness that waits for each response measures its own latency and can miss the limit entirely.
The rejected requests came back faster than the accepted ones. Across the five each way, the 200s averaged about 1,913 ms and the 429s about 1,002 ms [1], a gap of roughly 900 ms [2]. That gap is the round trip a rejected request never makes: out of the gateway, through the Cloudflare tunnel, into a laptop and back [2][16]. The author is explicit that response timing alone cannot prove where a request was stopped [16].
A fixed schedule mattered more than the interval between requests. All ten went out inside 1.806 seconds [3], and the first response did not land until 2,171 ms after request 1 [11], so the whole burst was in flight before any answer arrived [6]. In the first run each request waited for its predecessor. When two of them hung for a full minute on an unexplained gateway timeout, everything queued behind them fell into later minutes where the count had already reset [5][6].
Establishing the window type was harder. At 12:14:02 the `x-ratelimit-reset` header already read 57, counting down to the top of the minute [14]. A rolling window anchored to the first request would have read 60, three seconds more [4]. Three seconds is thin separation when single-request latencies in that same log ran from 964 to 2,171 ms [11]. The stronger reading came out of the botched first run. A new count opened there at 12:03:10 and reported `reset=49`, pointing at 12:03:59 [15], eleven seconds short of what a rolling window would say [5].
Firing two seconds after a new minute began [8] kept all ten requests inside one window and made the cutoff at five clean. It also left the reset header barely able to discriminate. A test aimed at the window type should start mid-minute, where the two models predict numbers tens of seconds apart.
That kind of trap shows up in the second correction. The author withdrew a conclusion he had drawn from a follow-up request sent after the burst. It landed 59.993 seconds after request 1, seven milliseconds short of a full 60-second window, inside the timing variation already visible in the log [13].
Another GET endpoint on the same backend has no rate limiting. When the author needed that backend publicly reachable for separate testing, he switched the endpoint off [1].
For any of this to transfer, the environment has to match. The limit was configured in a numeric field with a time-unit dropdown [3], on a -dev host with a sandbox-scoped key, and the author flags that production deployments could behave differently [4]. The latency numbers describe the rig, not the product. The header semantics are what I would build a client against, and I would re-run the two reset readings against a production host before trusting them there.
One measurement is independent of timing: the tunnel's own request counter, which increases only when a request actually reaches the machine [16]. The account sets up that test but never reports what the counter read [17].
What to watch
- Whether the tunnel's request counter shows zero increments for the five rejected calls, which would place the rejection at the gateway and confirm it never reaches the backend.
- Whether the same x-ratelimit-reset behaviour holds on a Bijira production host, given the test ran on a -dev host with a sandbox-scoped key.
- Whether the unexplained 60-second gateway timeouts seen in the first run recur outside the sandbox environment.