Skip to content

Ask the HotKV docs

Answers come only from these docs and can be wrong; check the linked pages before you rely on them.

Rate limiting

Last updated

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.

All Cloud plansOn-Premise: no license neededCommunity build
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 10
1) (integer) 0
2) (integer) 10
3) (integer) 9
4) (integer) -1
5) (integer) 6000

After ten quick calls, the eleventh is refused:

127.0.0.1:6379> GCRA.LIMIT api:user:42 60000 10
1) (integer) 1
2) (integer) 10
3) (integer) 0
4) (integer) 6000
5) (integer) 60000

A 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")
Cloud: Performance and ScaleOn-Premise: rate-limiting license

RATELIMIT.* 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
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

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 60000
OK
127.0.0.1:6379> RATELIMIT.CHECK api user:42
1) (integer) 1
2) (integer) 99
3) (integer) 0
4) (integer) 100
5) (integer) 0
127.0.0.1:6379> RATELIMIT.CHECK api user:42 COST 5
1) (integer) 1
2) (integer) 94
3) (integer) 0
4) (integer) 100
5) (integer) 1
127.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) 1
11) "total_allowed"
12) (integer) 6
13) "total_denied"
14) (integer) 0

A 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 5
OK

RATELIMIT.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")
  • Use GCRA.LIMIT for 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-only PEEK, or allowed and denied totals per limiter.

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.