Different algorithms answer retry-after from different clocks. For the fixed window the wait is simply the time until the current window ends and the count resets. Today you compute it and return it on every denied request.
On a denied fixed-window request, report the ticks until the window resets.
Retry-after depends on why a limiter is denying, so each algorithm computes it
differently. The token bucket denies because it is short of tokens, so its wait came
from the refill rate. The fixed window denies because the current window is full, and
that clears at a fixed moment: the end of the window. The wait is therefore
(curWindow + 1) * window - now - the next window boundary minus the current tick -
and it needs no rounding because window boundaries fall on exact ticks.
Pin it at two points in the same window. Denied at tick 3, the window (index 0) ends at tick 10, so the client should retry in 7 ticks. Denied at tick 9, only 1 tick remains, so the hint is 1 - and indeed tick 10 opens a fresh window, exactly as the retry-after promised. Two limiters, two very different sources for the same piece of advice, both now honest to the tick. This is the last piece of the individual algorithms; next you make them interchangeable.
func (f *FixedWindow) Allow(now int64) Decision {w := now / f.windowif w != f.curWindow { f.curWindow, f.count = w, 0 }if f.count < f.limit {f.count++return Decision{Allowed: true}}wait := (f.curWindow+1)*f.window - now // ticks until this window endsreturn Decision{Allowed: false, RetryAfter: wait}}