Rate Limiters Are Not Your Enemy: How Token Buckets Work and How Automation Should Behave Under Them
Rate limiters are having a quiet moment — the topic sits at #12 on daily.dev Popular this morning — and for good reason: it is how most serious automation actually moves. Every pip
- A limiter is a budget, not a wall
- The four limiters you will actually meet
- Read the headers before you retry
<span class="k">def</span> <span class="nf">call_with_limit_awareness</span><span class="p">(</span><span class="n">http_call</span><span class="p">,</span> <span class="n">max_attempts</span><span class="o">=</span><span class="mi">5</span><span class="p">):</span> <span class="k">for</span> <span class="n">attempt</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">max_attempts</span><span class="p">):</span> <span class="n">resp</span> <span class="o">=</span> <span class="nf">http_call</span><span class="p">()</span> <span class="k">if</span> <span class="n">resp</span><span class="p">.</span><span class="n">status_code</span> <span class="o">!=</span> <span class="mi">429</span><span class="p">:</span> <span class="k">return</span> <span class="n">resp</span> <span class="c1"># Respect the server's own number first. </span> <span class="n">retry_after</span> <span class="o">=</span> <span class="n">resp</span><span class="p">.</span><span class="n">headers</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">Retry-After</span><span class="sh">"</span><span class="p">)</span> <span class="k">if</span> <span class="n">retry_after</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span> <span class="n">time</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="nf">float</span><span class="p">(</span><span class="n">retry_after</span><span class="p">))</span> <span class="k">continue</span> <span class="c1"># No Retry-After: back off exponentially, with jitter, </span> <span class="c1"># so a fleet of clients does not retry in lockstep. </span> <span class="n">delay</span> <span class="o">=</span> <span class="nf">min</span><span class="p">(</span><span class="mf">60.0</span><span class="p">,</span> <span class="p">(</span><span class="mi">2</span> <span class="o">**</span> <span class="n">attempt</span><span class="p">))</span> <span class="o">+</span> <span class="n">random</span><span class="p">.</span><span class="nf">uniform</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="n">time</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="n">delay</span><span class="p">)</span> <span class="k">return</span> <span class="n">resp</span> </code></pre>
</div> <p>Two details in that snippet do most of the work. First, <code>Retry-After</code> wins over any backoff formula you invented, because the server knows its own limiter and you do not. Second, the jitter matters as much as the backoff: without it, every client that hit the limit at the same second retries at the same second, and the limiter gets a second burst exactly when it is most sensitive. Our <a href="https://botslave.com/blog/event-driven-pipeline-failure-modes" rel="noopener noreferrer">event-driven pipelines</a> piece makes the same argument for poison-message retries — jitter is what turns a herd into a queue.</p> <h2>- Budget your own tokens, client-side
<span class="k">class</span> <span class="nc">TokenBucket</span><span class="p">:</span> <span class="k">def</span> <span class="nf">init</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">capacity</span><span class="p">,</span> <span class="n">refill_per_second</span><span class="p">):</span> <span class="n">self</span><span class="p">.</span><span class="n">capacity</span> <span class="o">=</span> <span class="n">capacity</span> <span class="n">self</span><span class="p">.</span><span class="n">tokens</span> <span class="o">=</span> <span class="n">capacity</span> <span class="n">self</span><span class="p">.</span><span class="n">refill</span> <span class="o">=</span> <span class="n">refill_per_second</span> <span class="n">self</span><span class="p">.</span><span class="n">updated_at</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="nf">monotonic</span><span class="p">()</span> <span class="n">self</span><span class="p">.</span><span class="n">lock</span> <span class="o">=</span> <span class="n">threading</span><span class="p">.</span><span class="nc">Lock</span><span class="p">()</span>
text<span class="k">def</span> <span class="nf">acquire</span><span class="p">(</span><span class="n">self</span><span class="p">):</span> <span class="k">with</span> <span class="n">self</span><span class="p">.</span><span class="n">lock</span><span class="p">:</span> <span class="n">now</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="nf">monotonic</span><span class="p">()</span> <span class="n">elapsed</span> <span class="o">=</span> <span class="n">now</span> <span class="o">-</span> <span class="n">self</span><span class="p">.</span><span class="n">updated_at</span> <span class="n">self</span><span class="p">.</span><span class="n">tokens</span> <span class="o">=</span> <span class="nf">min</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">capacity</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">tokens</span> <span class="o">+</span> <span class="n">elapsed</span> <span class="o">*</span> <span class="n">self</span><span class="p">.</span><span class="n">refill</span><span class="p">)</span> <span class="n">self</span><span class="p">.</span><span class="n">updated_at</span> <span class="o">=</span> <span class="n">now</span> <span class="k">if</span> <span class="n">self</span><span class="p">.</span><span class="n">tokens</span> <span class="o">>=</span> <span class="mi">1</span><span class="p">:</span> <span class="n">self</span><span class="p">.</span><span class="n">tokens</span> <span class="o">-=</span> <span class="mi">1</span> <span class="k">return</span> <span class="bp">True</span> <span class="n">wait</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">self</span><span class="p">.</span><span class="n">tokens</span><span class="p">)</span> <span class="o">/</span> <span class="n">self</span><span class="p">.</span><span class="n">refill</span> <span class="n">time</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="n">wait</span><span class="p">)</span> <span class="n">self</span><span class="p">.</span><span class="n">tokens</span> <span class="o">=</span> <span class="mi">0</span> <span class="n">self</span><span class="p">.</span><span class="n">updated_at</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="nf">monotonic</span><span class="p">()</span> <span class="k">return</span> <span class="bp">True</span>
</code></pre>
</div> <p>Three rules make a client-side bucket actually work in production:</p> <ol> <li> <strong>Size it below the documented limit, not at it.</strong> If the API documents 100 requests per minute, run your bucket at 80. The margin absorbs clock skew, undocumented secondary limits, and the requests your other jobs are making against the same key that you forgot about.</li> <li> <strong>One bucket per key, not per job.</strong> If three jobs share an API key and each runs its own bucket at 80/minute, you are making 240/minute against a 100/minute limit. The bucket must live where the key lives — a shared service, a Redis key, a single coordinator — not inside each job's memory.</li> <li> <strong>Treat the bucket as a queue, not a gate.</strong> When the bucket is empty, the correct behaviour for a scraper is usually to wait, not to fail. Failing on an empty bucket turns a rate limit into a data gap; waiting turns it into a slower run. The exception is interactive work, where a human is waiting — there, fail fast and say why, with the reset time in the message.</li> </ol> <h2>- Distributed limiters and the limits of client-side politeness
- The pre-flight checklist
FAQ
</h2> <p><strong>Is a token bucket the same as a fixed window?</strong><br> No. A fixed window resets its whole budget at a boundary, which allows boundary bursts. A token bucket refills steadily, so the budget comes back gradually, and a burst is capped by the bucket's capacity rather than by the clock.</p> <p><strong>Should I just retry immediately when I get a 429?</strong><br> No. Immediate retries are the most common way to turn a short slowdown into a long ban. Respect <code>Retry-After</code> when present; otherwise back off exponentially with jitter, and cap the number of attempts.</p> <p><strong>Why does my client get limited even though it is under the documented rate?</strong><br> Usually one of three reasons: a secondary limit (per-endpoint, per-minute, or concurrency) that is not the headline number; another job sharing the same key; or an abuse heuristic that is not rate-based at all. Check the response headers and body before assuming the headline limit is wrong.</p> <p><strong>Do I need a distributed limiter, or is a local bucket enough?</strong><br> A local bucket is enough for one process against one key. As soon as multiple jobs or machines share a key, the bucket has to be shared too, or each job's "safe" rate adds up to an unsafe total.</p> <h2>References
</h2> <ul> <li>RFC 6585, Additional HTTP Status Codes (defines 429 Too Many Requests): <a href="https://www.rfc-editor.org/rfc/rfc6585" rel="noopener noreferrer">https://www.rfc-editor.org/rfc/rfc6585</a> </li> <li>MDN Web Docs, 429 Too Many Requests and the Retry-After header: <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/429" rel="noopener noreferrer">https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/429</a> </li> <li>GitHub REST API documentation, Rate limits (primary limits, secondary limits, and Retry-After behaviour): <a href="https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api" rel="noopener noreferrer">https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api</a> </li> <li>Stripe API documentation, Rate limits (idempotency and limit behaviour): <a href="https://docs.stripe.com/api/rate-limits" rel="noopener noreferrer">https://docs.stripe.com/api/rate-limits</a> </li> <li>Cloudflare documentation, Rate limiting rules: <a href="https://developers.cloudflare.com/waf/rate-limiting-rules/" rel="noopener noreferrer">https://developers.cloudflare.com/waf/rate-limiting-rules/</a> </li> <li>Botslave, "Shopee Security Headers: TLS, Tokens & Bot Detection": <a href="https://botslave.com/blog/shopee-security-headers-tls-tokens-bot-detection" rel="noopener noreferrer">https://botslave.com/blog/shopee-security-headers-tls-tokens-bot-detection</a> </li> <li>Botslave, "Event-Driven Pipelines Fail in Five Predictable Ways — and How to Design Against Them": <a href="https://botslave.com/blog/event-driven-pipeline-failure-modes" rel="noopener noreferrer">https://botslave.com/blog/event-driven-pipeline-failure-modes</a> </li> <li>Botslave, "How we work": <a href="https://botslave.com/how-we-work" rel="noopener noreferrer">https://botslave.com/how-we-work</a> </li> </ul> <p><em>Originally published on <a href="https://botslave.com/blog/rate-limiter-token-bucket-automation" rel="noopener noreferrer">botslave.com</a>. Botslave is an automation studio — see <a href="https://botslave.com/services" rel="noopener noreferrer">what we build</a> and <a href="https://botslave.com/how-we-work" rel="noopener noreferrer">how we work</a>.</em></p>Original source
Tags
Related Articles
Continue reading with these similar posts

Sources: Missouri RB Hardy, shot in offseason, to return vs. Arkansas
Missouri All-America tailback Ahmad Hardy is set to make his season debut for the Tigers this weekend at Arkansas, sources told ESPN.

Red Wings suspend Larkin as team explores trade options; players keen to move on
The Red Wings announced Thursday that star forward Dylan Larkin has been suspended without pay after informing the team he no longer wanted to play for Detroit.

Packers' Jacobs says TD celebration was ode to gospel song, not mocking arrest
The Packers' Josh Jacobs said he was surprised his first-half TD celebration was perceived by some as mocking his May arrest, saying it was an ode to a gospel song.

Israel faces weeks of coalition haggling as Netanyahu’s party claims election 'stolen'
Israel's election leaves the country facing weeks of coalition bargaining and an uphill battle for the opposition party to form a majority government.
Herdr has released native app
Comments

Satellite vs Fiber Internet: ഇന്ത്യയുടെ ഇന്റർനെറ്റ് വിപ്ലവത്തിന് ആര് മികച്ചത്?
ഇന്ന് ഇന്റർനെറ്റ് ഒഴിച്ചുകൂടാനാവാത്ത ഭാഗമായി മാറിക്കഴിഞ്ഞു. ഇപ്പോൾ ഇന്ത്യയിൽ നിലവിൽ ഏറ്റവും ജനപ്രിയമായ ഇന്റർനെറ്റ് മാധ്യമം ഒപ്റ്റിക്കൽ ഫൈബർ (Fiber Internet) ആണ്. എന്നാൽ രാജ്യത്ത് സജീവ ചർച്ചയായി മാറിക്