Rate limiting
Rate limiting caps how often something may happen: API calls per user, login attempts per IP address, messages per channel. Doing the check on the server, in one command, means every application instance shares the same count and no two requests can slip through on the same stale value.
HotKV has two built-in options:
- GCRA.LIMIT, a single command on any key, available on every plan and build.
RATELIMIT.*, named limiters with a choice of algorithm and per-limiter statistics, an Enterprise feature.
Both replace the Lua scripts or INCR plus EXPIRE patterns commonly used
with Redis, which has no rate limiting command of its own.
GCRA.LIMIT
Section titled “GCRA.LIMIT”GCRA.LIMIT key period burst [count]GCRA.LIMIT allows up to burst requests per period milliseconds for each
key, spread evenly: with a period of 60000 and a burst of 10, a client can
make 10 requests at once, then one more every 6 seconds. count (default 1)
is how many units this request uses. It replies with five integers:
| Position | Field | Meaning |
|---|---|---|
| 1 | denied |
0 if the request is allowed, 1 if it is refused |
| 2 | limit |
The burst size you passed |
| 3 | remaining |
Requests still allowed right now |
| 4 | retry_after_ms |
When denied, milliseconds until this request would be allowed; -1 when allowed |
| 5 | reset_after_ms |
Milliseconds until the key is back to a full burst |
127.0.0.1:6379> GCRA.LIMIT api:user:42 60000 101) (integer) 02) (integer) 103) (integer) 94) (integer) -15) (integer) 6000After ten quick calls, the eleventh is refused:
127.0.0.1:6379> GCRA.LIMIT api:user:42 60000 101) (integer) 12) (integer) 103) (integer) 04) (integer) 60005) (integer) 60000A denied request uses nothing, so retrying after retry_after_ms succeeds.
GCRA.LIMIT keeps its state in key, which expires
by itself once the key is back to a full burst. A key that already holds
something else (another type, or a string that is not a number) is refused
with WRONGTYPE and left alone. GCRA.RESET
key [key ...] deletes the state of one or more keys and replies with how
many existed. period, burst and count must be positive integers.
Limits carry over correctly to replicas, through restarts, and across a failover.
import hotkv
client = hotkv.HotKV(host="localhost", port=6379)
result = client.gcra_limit("api:user:42", 60000, 10)if result.denied: print(f"slow down, retry in {result.retry_after_ms} ms")else: print(f"{result.remaining} requests left")import { HotKV } from "@hotkv/client";
const client = new HotKV({ url: "hotkv://127.0.0.1:6379" });
const result = await client.gcraLimit("api:user:42", 60000, 10);if (result.denied) { console.log(`slow down, retry in ${result.retryAfterMs} ms`);} else { console.log(`${result.remaining} requests left`);}result, err := client.GCRALimit(ctx, "api:user:42", time.Minute, 10, 1).Result()if err != nil { log.Fatal(err)}if result.Denied { fmt.Printf("slow down, retry in %d ms\n", result.RetryAfterMs)} else { fmt.Printf("%d requests left\n", result.Remaining)}var result = client.gcra().limit("api:user:42", 60_000, 10);if (result.denied()) { System.out.println("slow down, retry in " + result.retryAfterMillis() + " ms");} else { System.out.println(result.remaining() + " requests left");}RATELIMIT.* limiters
Section titled “RATELIMIT.* limiters”rate-limiting licenseRATELIMIT.* manages named limiters. You create a limiter once with its
algorithm and limits, then check any number of keys (user ids, IP addresses,
API keys) against it. Each limiter keeps its own allowed and denied totals.
| Command | What it does |
|---|---|
RATELIMIT.CREATE name limit window_ms [ALGO algo] [REFILL rate] [DRAIN rate] |
Create a limiter |
RATELIMIT.CHECK name key [COST n] |
Check a request for key and count it if allowed |
RATELIMIT.PEEK name key |
Same reply as CHECK, without counting anything |
RATELIMIT.RESET name key |
Forget one key’s state |
RATELIMIT.STATS name |
The limiter’s settings, number of keys, and allowed/denied totals |
RATELIMIT.DROP name |
Delete the limiter and all its state |
| RATELIMIT.LIST | The names of all limiters |
Algorithms
Section titled “Algorithms”ALGO |
Behavior |
|---|---|
sliding_window (default) |
At most limit units in any window_ms period, without the burst at the edge that fixed windows allow |
token_bucket |
A bucket of limit tokens, refilled at REFILL tokens per second (default limit per window_ms); each request takes tokens, so bursts up to limit are allowed |
leaky_bucket |
A bucket of size limit that drains at DRAIN units per second (default limit per window_ms); each request adds to it, which smooths traffic to a steady rate |
Checking requests
Section titled “Checking requests”RATELIMIT.CHECK replies with five integers: allowed (1 or 0),
remaining, retry_after_ms (0 when allowed), limit, and current (the
usage before this request).
127.0.0.1:6379> RATELIMIT.CREATE api 100 60000OK127.0.0.1:6379> RATELIMIT.CHECK api user:421) (integer) 12) (integer) 993) (integer) 04) (integer) 1005) (integer) 0127.0.0.1:6379> RATELIMIT.CHECK api user:42 COST 51) (integer) 12) (integer) 943) (integer) 04) (integer) 1005) (integer) 1127.0.0.1:6379> RATELIMIT.STATS api 1) "name" 2) "api" 3) "algo" 4) "sliding_window" 5) "limit" 6) (integer) 100 7) "window_ms" 8) (integer) 60000 9) "unique_keys"10) (integer) 111) "total_allowed"12) (integer) 613) "total_denied"14) (integer) 0A token bucket allowing bursts of 20 and a steady 5 requests per second:
127.0.0.1:6379> RATELIMIT.CREATE uploads 20 4000 ALGO token_bucket REFILL 5OKRATELIMIT.CREATE fails with ERR limiter already exists or limit reached
when the name is taken or the server already holds its maximum number of
limiters (ratelimit.max-limiters in the enterprise configuration).
RESET, STATS and DROP reply ERR limiter not found for an unknown name.
Limiters and their per-key state are saved with snapshots and kept in step on
replicas. In a cluster, a limiter lives on the node that serves the slot of
its name, and all of its keys are checked there; RATELIMIT.LIST lists the
limiters of the node you ask.
client.ratelimit_create("api", 100, 60000)
check = client.ratelimit_check("api", "user:42", cost=1)if not check.allowed: print(f"retry in {check.retry_after_ms} ms")await client.rateLimitCreate("api", 100, 60000);
const check = await client.rateLimitCheck("api", "user:42");if (!check.allowed) { console.log(`retry in ${check.retryAfterMs} ms`);}client.RateLimitCreate(ctx, "api", 100, time.Minute, nil)
check, err := client.RateLimitCheck(ctx, "api", "user:42", 1).Result()if err == nil && !check.Allowed { fmt.Printf("retry in %d ms\n", check.RetryAfterMs)}client.rateLimit().create("api", 100, 60_000, null, null, null);
var check = client.rateLimit().check("api", "user:42", null);if (!check.allowed()) { System.out.println("retry in " + check.retryAfterMillis() + " ms");}Choosing between them
Section titled “Choosing between them”- Use
GCRA.LIMITfor most request throttling. It needs no setup, works on every plan, and each key is an ordinary key that expires by itself. - Use
RATELIMIT.*when you want a choice of algorithm, cost-based checks against shared limiter settings, a read-onlyPEEK, or allowed and denied totals per limiter.
Patterns
Section titled “Patterns”Per user and per endpoint. Put both in the key, for example
rl:{user:42}:search, so each user has a separate budget for each endpoint.
The hash tag keeps one user’s limits on the same cluster node.
Several limits at once. Check a short and a long window, for example 10 per second and 1000 per hour, and allow the request only if both allow it. Check the stricter one first to avoid using up the other on requests that will be refused anyway.
Tell the client when to retry. Return retry_after_ms to the caller, for
example as an HTTP Retry-After header (in seconds, rounded up), so
well-behaved clients wait instead of retrying in a tight loop.
Weighted requests. Pass count to GCRA.LIMIT, or COST to
RATELIMIT.CHECK, to charge expensive operations more than cheap ones.
Was this page helpful?
Thanks for the feedback.