Skip to content

fix(limit-count): make sliding-window limiter check-and-increment atomic#13574

Open
shreemaan-abhishek wants to merge 1 commit into
apache:masterfrom
shreemaan-abhishek:fix/limit-count-sliding-window-atomic
Open

fix(limit-count): make sliding-window limiter check-and-increment atomic#13574
shreemaan-abhishek wants to merge 1 commit into
apache:masterfrom
shreemaan-abhishek:fix/limit-count-sliding-window-atomic

Conversation

@shreemaan-abhishek

Copy link
Copy Markdown
Contributor

Description

The limit-count sliding-window limiter read the current counter, evaluated the limit (including the smoothed carry-over from the previous window), and only then incremented, as three separate steps. With the redis stores the get and incr are separate round trips, so under concurrency multiple requests can all pass the check before any increment is observed and exceed the configured rate.

This folds the accept/reject decision and the increment into a single atomic store operation, check_and_incr:

  • redis / redis-cluster / redis-sentinel: a Lua script that reads both windows, decides, and increments only on accept, in one server-side step.
  • local (shared dict): a single non-yielding get/decide/incr sequence.

The existing contract is preserved: incoming() still never increments on reject (verified by the delayed-sync regression test), while commit() keeps flushing already-permitted deltas. The previous reactive post-increment re-check is no longer needed and is removed.

Because the redis script now touches two keys (current and previous window), the counter key carries a {...} hash tag so both windows hash to the same redis-cluster slot.

Which issue(s) this PR fixes:

N/A (hardening; reported internally against the API7 fork)

Checklist

  • I have explained the need for this PR and the problem it solves
  • I have explained the changes or the new features added to this PR
  • I have added tests corresponding to this change
  • I have updated the documentation to reflect this change (internal correctness fix; no API/behavior change)
  • I have verified that this change is backward compatible

The sliding window read the counter, evaluated the limit, then incremented
in separate steps. With redis the get and incr are separate round trips, so
concurrent requests could all pass the check before any increment landed and
exceed the configured rate.

Fold the decision and the increment into one atomic store operation
(check_and_incr): a redis Lua script for the redis-backed stores and a single
non-yielding sequence for the shared-dict store. It increments only when the
request is accepted, preserving the existing contract that incoming() never
increments on reject (commit() still flushes delayed-sync deltas).

The redis script touches two keys (current and previous window), so the
counter key now carries a hash tag to keep both on one redis-cluster slot.
@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Jun 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant