Fixed Window Counter

Time is cut into windows of equal length, each with a counter. A request is counted and let through while the counter is under the limit, and refused after that until the counter resets to zero at the next boundary. Below, an attacker joins two ordinary users: watch who is refused when they share one counter and when each has their own, and what happens when the attacker times bursts around a boundary.

User A289 req/sUser B289 req/sAttacker1,736 req/sAPI serverprotected

Each dot stands for 579 real requests. Time is not scaled. The busiest stretch counts what got through in any 5 seconds in a row, not only in the windows as the counter sees them. That is how the boundary problem shows up.

Sizing the shared counter
x average

Busiest hour vs the daily average. The book's estimate uses 2x.

seconds

Real limits often use a minute or an hour; the animation stays short so you can watch windows go by.

  1. 1. Average requests per second

    DAU x requests per user per day / 86,400
    = 1,000,000 x 50 / 86,400 = 579 req/s
  2. 2. Peak requests per second

    average x peak factor = 579 x 2 = 1,157 req/s
  3. 3. Allowed rate r

    r = peak = 1,157 req/s
    The average rate a full window lets through.
  4. 4. Limit per window L

    L = r x window length = 1,157 x 5 s = 5,788 requests
  5. 5. What is arriving right now

    average x (1 + attack / 2) = 579 x (1 + 6 / 2) = 2,315 req/s, which is 2.00x r
  6. Requests arrive faster than r, so each window's 5,788 are used up in L / live = 5,788 / 2,315 = 2.5 s, and the other 2.5 s of the window everything is refused. Over time about 50% of requests are refused (1 - r / live). With a shared counter, that includes the innocent users. At any boundary, up to 2L = 11,575 can pass in a span much shorter than one window.
Why changing only DAU does not change the animation

Doubling DAU doubles the traffic, and because r and L are sized from that same traffic, it doubles them too. Every ratio stays the same, so the counter behaves identically and only the number each dot stands for changes. What changes the behaviour is traffic relative to r: the peak factor and the attack. The window length changes how long each refused stretch lasts.

Where these formulas come from

Steps 1 and 2 are the estimation method from the back-of-the-envelope chapter of Alex Xu's System Design Interview, Vol. 1, including its 2x peak. A window length and a limit per window are the fixed window counter's two parameters in its rate limiter chapter, which also points out the boundary weakness shown here. The book does not give a single formula joining the two; steps 3 to 5 are that join, written here. The 2L bound follows from the reset: each of two neighbouring windows may let through L.

Limits of fixed windows

Up to twice the limit at a boundary

The counter only knows about its own window. Requests just before a boundary and just after it are counted in different windows, so up to 2L pass in a moment. Choose Bursts at boundaries and watch the busiest stretch. Sliding window algorithms exist to close this gap.

Refusals come in clumps

Once a window is used up, every request is refused until the boundary, and then a crowd of retries arrives at once. Clients told when the window resets tend to retry at that exact moment, so the next window can fill almost immediately.

Cheapest to run

One number per key, and the key can carry the window itself, so old counts simply expire. No queue, no list of timestamps, no refill arithmetic. That is why it is often the first limiter people build.

Use both limits together

Per-user counters keep one client from using up the others' windows; a global limit protects total capacity. Turn on Attacker rotates accounts: every request lands in a fresh counter, and only a global limit would notice.

How Redis and GitHub use fixed windows

One shows how to build a fixed window counter, the other how a large API reports a windowed limit to clients.

Redis: a fixed window from INCR and EXPIRE

The Redis documentation for the INCR command includes a rate limiter pattern built exactly this way. The key is the client's IP address joined to the current Unix time in seconds, so every second starts a new key, and with it a new count. Each request runs INCR on that key and sets EXPIRE to 10 seconds so old keys clean themselves up; if the count is over 10, the request is refused with an error. That is a per-IP counter with a one-second window and a limit of 10. Putting the window in the key means there is nothing to reset: the next second simply uses a different key.

GitHub: a limit per hour, with a reset time

GitHub's REST API counts requests per hour: 5,000 for an authenticated user and 60 for unauthenticated requests, which are counted by IP address, with higher limits on some GitHub Enterprise Cloud accounts. Every response says how many requests are left, and the x-ratelimit-reset header is documented as "The time at which the current rate limit window resets in UTC epoch seconds." That is how GitHub reports its limit to clients; the documentation does not describe how it is counted internally, so read it as the client's view of a windowed limit, not as GitHub's implementation.
Sources and credibility

Both are the projects' own documentation (primary sources). The quoted sentence is a short attributed quotation; the Redis pattern is described in our words. Limits change, so check the pages for current values.