# `Hammer.ETS.FixWindowPerKey`
[🔗](https://github.com/ExHammer/hammer/blob/v7.4.0/lib/hammer/ets/fix_window_per_key.ex#L1)

This module implements a per-key fixed window rate-limiting algorithm.

Like the standard fixed window algorithm, requests are counted within a window of
duration `scale`. Unlike the standard fixed window — which aligns every key to the
same wall-clock boundary (multiples of `scale` since the Unix epoch) — this variant
anchors each key's window to that key's **first hit**.

For example, with a scale of 60 seconds:

- User A's first request at `12:00:37` → A's window runs until `12:01:37`
- User B's first request at `12:00:51` → B's window runs until `12:01:51`

Once a key's window expires, the next hit for that key opens a fresh window starting
at that moment.

## The algorithm

1. When a request comes in for a key:
   - If the key has an active window (`expires_at > now`), increment its counter.
   - Otherwise, start a new window: reset the counter to `increment` and set
     `expires_at = now + scale`.
2. If the counter is `<= limit` → allow. Otherwise → deny and return time until the
   current window expires.
3. Expired entries are cleaned up by the periodic cleanup task.

## When to use this vs `:fix_window` and `:sliding_window`

The 2x boundary burst that affects `:fix_window` is **still theoretically possible**
here, just at a per-key boundary instead of a globally synchronized one. Example:

- 100 requests at `12:00:37` (window `[12:00:37, 12:01:37)`)
- 100 more at `12:01:38` (window `[12:01:38, 12:02:38)`)
- That's 200 requests in roughly one second.

The practical benefit is that boundaries are not globally synchronized. With
`:fix_window`, every key flips at the same wall-clock instant (e.g. every minute on
the minute), so an attacker who knows your `scale` can time bursts deterministically.
With `:fix_window_per_key`, each key's boundary is at a different moment, and a key
has to wait a full `scale` between burst opportunities.

Choose `:fix_window_per_key` when:

- You want `:fix_window`-style simplicity and the same one-entry-per-key memory
  footprint, but find globally-synchronized boundaries undesirable.
- You're familiar with the Redis `INCR + EXPIRE NX` rate-limiting pattern — this is
  essentially the same algorithm.

Choose `:fix_window` when boundaries aligned to the wall clock are useful for your
use case (clear time-based quotas like "100 requests per minute, starting on the
minute").

Choose `:sliding_window` when you need precise enforcement — never more than `limit`
in *any* `scale`-length interval. It costs more memory (one entry per request) and
CPU per check, but eliminates the boundary burst entirely.

## Options

- `:clean_period` - How often to run the cleanup process (in milliseconds). Defaults
  to 1 minute. The cleanup process removes expired entries.

## Example

### Example configuration:

    MyApp.RateLimit.start_link(
      clean_period: :timer.minutes(5),
    )

### Example usage:

    defmodule MyApp.RateLimit do
      use Hammer, backend: :ets, algorithm: :fix_window_per_key
    end

    MyApp.RateLimit.start_link(clean_period: :timer.minutes(1))

    # Allow 10 requests per second
    MyApp.RateLimit.hit("user_123", 1000, 10)

# `clean`

```elixir
@spec clean(config :: Hammer.ETS.config()) :: non_neg_integer()
```

Cleans up all of the expired entries from the table.

# `expires_at`

```elixir
@spec expires_at(table :: atom(), key :: term(), scale :: pos_integer()) ::
  non_neg_integer()
```

Returns the expiration time (in milliseconds) of the current window for a given key.

Returns `0` if the key has no active window.

# `get`

```elixir
@spec get(table :: atom(), key :: term(), scale :: pos_integer()) :: non_neg_integer()
```

Returns the current count for a given key.

Returns `0` if the key has no active window (either never hit, or window has expired).

# `hit`

```elixir
@spec hit(
  table :: atom(),
  key :: term(),
  scale :: pos_integer(),
  limit :: pos_integer(),
  increment :: pos_integer()
) :: {:allow, non_neg_integer()} | {:deny, non_neg_integer()}
```

Checks if a key is allowed to perform an action based on the per-key fixed window
algorithm.

# `inc`

```elixir
@spec inc(
  table :: atom(),
  key :: term(),
  scale :: pos_integer(),
  increment :: pos_integer()
) :: non_neg_integer()
```

Increments the counter for a given key without performing a limit check.

# `set`

```elixir
@spec set(
  table :: atom(),
  key :: term(),
  scale :: pos_integer(),
  count :: non_neg_integer()
) ::
  non_neg_integer()
```

Sets the counter for a given key, refreshing the window to `now + scale`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
