@@ -140,8 +140,9 @@ Every rule in the limiter is cleared, matched or not, because a caller
clearing state after a success knows the identifier rather than which rules
happened to match on the way in. Each rule is deleted in its own call, since
rules do not share a Redis Cluster slot. Clearing an identifier with no state
is a no-op returning `0`, and a Redis failure fails open like `check`,
returning `0` and leaving the state to expire on its own.
is a no-op returning `0`. A Redis failure fails open like `check`, leaving the
rest of the state to expire on its own, and the return value counts whatever
was removed before the failure.
## Identifier
@@ -173,7 +174,7 @@ A `Rule` is a `Data.define` value object with the following fields:
| `action` | What the rule does when it matches. One of `:limit`, `:log`, `:skip`. Default `:limit`. See [Actions](#actions). |
| `characteristics` | Array of identifier keys whose values are folded into the Redis counter key. Each unique combination gets its own counter. |
| `count_distinct` | Optional identifier key. When set, the rule counts distinct values seen for that key within the window, backed by a Redis SET rather than a counter. Must not overlap `characteristics`. |
| `ban_for` | Optional ban duration in seconds. Changes the accounting; `action` still decides who is blocked. Rejected on `:skip`. May be a callable resolved on every check. |
| `ban_for` | Optional ban duration, whole seconds, minimum 1. Changes the accounting; `action` still decides who is blocked. Rejected on `:skip`. May be a callable resolved on every check. |
Making `limit` or `period` callable is the supported pattern for
runtime-tunable thresholds (e.g. feature flags or database-backed settings):
@@ -356,6 +357,12 @@ window, and it stops counting while that ban holds:
while the next `check` re-bans immediately. Bans are normally longer than the
window they are counted in.
The duration is whole seconds and must be at least 1, since the ban is
written with `SET EX`. A callable is checked again each time it resolves, and
a value under a second raises rather than reaching Redis. Being an error, it
fails open like any other, so a rule whose `ban_for` cannot resolve stops
blocking entirely: watch `errors_total` after changing one.