<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Yash Suthar</title>
    <description>The latest articles on DEV Community by Yash Suthar (@trynayash).</description>
    <link>https://dev.to/trynayash</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4193532%2Fd8031337-4fce-43f9-a8f1-ed9336e59bca.png</url>
      <title>DEV Community: Yash Suthar</title>
      <link>https://dev.to/trynayash</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/trynayash"/>
    <language>en</language>
    <item>
      <title>How Redis rate limiters fail under load, measured</title>
      <dc:creator>Yash Suthar</dc:creator>
      <pubDate>Mon, 19 Oct 2026 09:58:34 +0000</pubDate>
      <link>https://dev.to/trynayash/how-redis-rate-limiters-fail-under-load-measured-2dn2</link>
      <guid>https://dev.to/trynayash/how-redis-rate-limiters-fail-under-load-measured-2dn2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Research paper:&lt;/strong&gt; &lt;a href="https://www.yashsuthar.dev/papers/redis-rate-limiting-under-concurrency.pdf" rel="noopener noreferrer"&gt;Overshoot, Retries and Clocks: An Empirical Study of Redis-Based Rate Limiting Under Concurrency&lt;/a&gt; · &lt;a href="https://www.yashsuthar.dev/papers/redis-rate-limiting-artifact.zip" rel="noopener noreferrer"&gt;code and data&lt;/a&gt; · DOI &lt;a href="https://doi.org/10.5281/zenodo.23467402" rel="noopener noreferrer"&gt;10.5281/zenodo.23467402&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I build rate limiting for a living, and most advice about Redis limiters says the same three things: don't race, beware fixed windows, use Lua. All true, but I couldn't find anyone who had measured how big each problem is. So I did: eight implementations, four experiments, 3,853 trials, and every number generated from the raw data. The full paper is &lt;a href="https://www.yashsuthar.dev/papers/redis-rate-limiting-under-concurrency.pdf" rel="noopener noreferrer"&gt;here&lt;/a&gt;; this is the short version.&lt;/p&gt;

&lt;h2&gt;
  
  
  The race has a formula
&lt;/h2&gt;

&lt;p&gt;The classic bug is &lt;code&gt;GET&lt;/code&gt;, compare, &lt;code&gt;INCR&lt;/code&gt;. Two workers read 99, both see room, both increment. With 64 concurrent workers and a limit of 100, it let through 128.7 requests on average, 29% more than the limit, and 137 in the worst trial.&lt;/p&gt;

&lt;p&gt;The surprise came with network delay. Once round trips are long, the workers fall into lockstep: each wave reads the same value and increments together, so the counter grows in steps of the worker count. The overshoot then follows a formula, &lt;code&gt;c·ceil(L/c) − L&lt;/code&gt;: 4 extra requests for 8 workers, 12 for 16, and 28 for both 32 and 64. The measurements matched it to within about one request. It also means the bug depends on how your limit divides by your worker count, so it can pass every test and appear after a config change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimistic transactions trade one failure for another
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;WATCH&lt;/code&gt;/&lt;code&gt;MULTI&lt;/code&gt; never over-admits: if another client touched the key, the transaction aborts and retries. But with a 10-retry budget, 64 workers and 6.7 ms round trips, it admitted only 58 of 100 requests. 42% of legitimate traffic was refused because the retries ran out. With 1,000 retries it was exact, but on a hot key it ran 12 to 30 times slower than a Lua script.&lt;/p&gt;

&lt;h2&gt;
  
  
  Atomic is not the same as keeping your promise
&lt;/h2&gt;

&lt;p&gt;Every atomic strategy admitted exactly 100 in all 2,520 of its concurrency trials. But "100 per second" means different things to different algorithms. Here is the most each one let through in any single second:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fixed window: 200 requests within a few milliseconds when two bursts straddle a window boundary, twice the limit.&lt;/li&gt;
&lt;li&gt;Token bucket (capacity 100, refill 100 per second): 1.99× the limit in one second after a quiet spell. That is by design, but it is not what a capacity plan assumes.&lt;/li&gt;
&lt;li&gt;Sliding-window counter: close, but 1.31× on average under a burst followed by sustained traffic, and 1.66× in the worst trial.&lt;/li&gt;
&lt;li&gt;Sliding-window log: exact under every pattern.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Never let callers supply the time
&lt;/h2&gt;

&lt;p&gt;A token bucket needs "now" to compute its refill. When the script took it from the calling server instead of Redis's &lt;code&gt;TIME&lt;/code&gt;, clock differences barely mattered under overload, at most 1.3% off. Near the limit, with ±1 s of skew between servers, the bucket collapsed and admitted only 16% of the traffic it should have. Clocks running behind produce negative refills that nothing caps, while positive refills are capped at the bucket's size, so the bucket drains. The same bucket on Redis's clock was exact in every setting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Make the decision one atomic step: a Lua script, or a single &lt;code&gt;INCR&lt;/code&gt; when the algorithm allows it.&lt;/li&gt;
&lt;li&gt;Choose the algorithm by the promise. Use a sliding-window log if downstream capacity means at most L in any window, and a token bucket when bursts are intended, with its capacity set as a burst budget.&lt;/li&gt;
&lt;li&gt;Read the time inside the script with &lt;code&gt;TIME&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Put &lt;code&gt;INCR&lt;/code&gt; and &lt;code&gt;EXPIRE&lt;/code&gt; in one step, so a crash can't leave a counter that never expires.&lt;/li&gt;
&lt;li&gt;Keep &lt;code&gt;WATCH&lt;/code&gt; away from hot keys.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Paper, data and code
&lt;/h2&gt;

&lt;p&gt;The paper covers the method, every table and figure, and the threats to validity: &lt;a href="https://www.yashsuthar.dev/papers/redis-rate-limiting-under-concurrency.pdf" rel="noopener noreferrer"&gt;read the PDF&lt;/a&gt;. It is archived on Zenodo with a permanent DOI, &lt;a href="https://doi.org/10.5281/zenodo.23467402" rel="noopener noreferrer"&gt;10.5281/zenodo.23467402&lt;/a&gt;, which is the link to cite. The harness, the raw data and the analysis are in &lt;a href="https://www.yashsuthar.dev/papers/redis-rate-limiting-artifact.zip" rel="noopener noreferrer"&gt;one download&lt;/a&gt;; &lt;code&gt;./run_all.sh&lt;/code&gt; reruns every experiment in about an hour.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.yashsuthar.dev/writing/redis-rate-limiter-failures-measured" rel="noopener noreferrer"&gt;yashsuthar.dev&lt;/a&gt;. I'm Yash Suthar, a backend developer in Mumbai building &lt;a href="https://limityourapi.tech" rel="noopener noreferrer"&gt;LimitYourAPI&lt;/a&gt;, &lt;a href="https://scopeci.onrender.com" rel="noopener noreferrer"&gt;ScopeCI&lt;/a&gt; and &lt;a href="https://startupdeck.in" rel="noopener noreferrer"&gt;StartupDeckAI&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>redis</category>
      <category>distributedsystems</category>
      <category>backend</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
