Interview question
Implement a simple fixed-window rate limiter
Implements a small in-memory fixed-window rate limiter with per-key counters.
TL;DR
Implements a small in-memory fixed-window rate limiter with per-key counters.
Dictionaries, time windows, counters, thread safety discussion, and API abuse-control reasoning.
Practice the problem like a real interview: restate, reason, implement, and test.
Implement Allow(string key, DateTimeOffset now) for a limiter with a permit limit and window duration. Return true when the request is allowed and false when the key has used all permits in the current window.
Next in Coding Practice: Async Retry Helper
Limit 3 per minute for key user-1: first three calls return true, fourth call in the same minute returns false, a call after the window resets returns true.
I would store one counter per key: window start and count. On each call, I validate the key, look up the counter, reset it if the window expired, then increment if the count is below the limit. I would use a lock for this simple in-memory version so two concurrent requests do not corrupt counts. I would also say that production usually needs a distributed counter such as Redis when multiple app instances exist.
public sealed class FixedWindowRateLimiter
{
private readonly int _permitLimit;
private readonly TimeSpan _window;
private readonly object _gate = new();
private readonly Dictionary<string, Counter> _counters = new(StringComparer.Ordinal);
public FixedWindowRateLimiter(int permitLimit, TimeSpan window)
{
if (permitLimit <= 0) throw new ArgumentOutOfRangeException(nameof(permitLimit));
if (window <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(window));
_permitLimit = permitLimit;
_window = window;
}
public bool Allow(string key, DateTimeOffset now)
{
if (string.IsNullOrWhiteSpace(key))
{
return false;
}
lock (_gate)
{
if (!_counters.TryGetValue(key, out var counter) ||
now - counter.WindowStart >= _window)
{
counter = new Counter(now, 0);
}
if (counter.Count >= _permitLimit)
{
_counters[key] = counter;
return false;
}
_counters[key] = counter with { Count = counter.Count + 1 };
return true;
}
}
private sealed record Counter(DateTimeOffset WindowStart, int Count);
}
Each call is O(1) average time and O(k) space for k keys. Without cleanup, space grows with the number of distinct keys seen.
now explicitly.Spot a weak answer, missing edge case, or clearer explanation? Send it in.