Skip to content

Commit 8aa5adf

Browse files
Fix token bucket implementation (#1795)
1 parent ede23d7 commit 8aa5adf

File tree

1 file changed

+25
-22
lines changed

1 file changed

+25
-22
lines changed

pages/rate-limit/token-bucket.md

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ title: "Token bucket"
44

55
# Token bucket
66

7-
Each user has their own bucket of tokens that gets refilled at a set interval. A token is removed on every request until none is left and the request is rejected. While a bit more complex than the fixed-window algorithm, it allows you to handle initial bursts and process requests more smoothly overall.
7+
Each user has their own bucket of tokens that gets refilled at a set interval. A token is removed on every request until none is left and the request is rejected. While a bit more complex than the fixed or sliding window algorithm, it allows you to handle initial bursts and process requests more smoothly overall.
88

99
## Memory storage
1010

@@ -33,10 +33,14 @@ export class TokenBucketRateLimit<_Key> {
3333
this.storage.set(key, bucket);
3434
return true;
3535
}
36-
const refill = Math.floor((now - bucket.refilledAt) / (this.refillIntervalSeconds * 1000));
36+
const refill = Math.floor(
37+
(now - bucket.refilledAtMilliseconds) / (this.refillIntervalSeconds * 1000)
38+
);
3739
bucket.count = Math.min(bucket.count + refill, this.max);
38-
bucket.refilledAt = bucket.refilledAt + refill * this.refillIntervalSeconds * 1000;
40+
bucket.refilledAtSeconds =
41+
bucket.refilledAtMilliseconds + refill * this.refillIntervalSeconds * 1000;
3942
if (bucket.count < cost) {
43+
this.storage.set(key, bucket);
4044
return false;
4145
}
4246
bucket.count -= cost;
@@ -47,15 +51,15 @@ export class TokenBucketRateLimit<_Key> {
4751

4852
interface Bucket {
4953
count: number;
50-
refilledAt: number;
54+
refilledAtMilliseconds: number;
5155
}
5256
```
5357

5458
```ts
55-
// Bucket that has 10 tokens max and refills at a rate of 2 tokens/sec
56-
const ratelimit = new TokenBucketRateLimit<string>(10, 2);
57-
58-
if (!ratelimit.consume(ip, 1)) {
59+
// Bucket that has 10 tokens max and refills at a rate of 30 seconds/token
60+
const ratelimit = new TokenBucketRateLimit<string>(5, 30);
61+
const valid = ratelimit.consume(ip, 1);
62+
if (!valid) {
5963
throw new Error("Too many requests");
6064
}
6165
```
@@ -70,38 +74,38 @@ local key = KEYS[1]
7074
local max = tonumber(ARGV[1])
7175
local refillIntervalSeconds = tonumber(ARGV[2])
7276
local cost = tonumber(ARGV[3])
73-
local now = tonumber(ARGV[4]) -- Current unix time in seconds
77+
local nowMilliseconds = tonumber(ARGV[4]) -- Current unix time in ms
7478

7579
local fields = redis.call("HGETALL", key)
7680

7781
if #fields == 0 then
7882
local expiresInSeconds = cost * refillIntervalSeconds
79-
redis.call("HSET", key, "count", max - cost, "refilled_at", now)
83+
redis.call("HSET", key, "count", max - cost, "refilled_at_ms", nowMilliseconds)
8084
redis.call("EXPIRE", key, expiresInSeconds)
8185
return {1}
8286
end
8387

8488
local count = 0
85-
local refilledAt = 0
89+
local refilledAtMilliseconds = 0
8690
for i = 1, #fields, 2 do
8791
if fields[i] == "count" then
8892
count = tonumber(fields[i+1])
89-
elseif fields[i] == "refilled_at" then
90-
refilledAt = tonumber(fields[i+1])
93+
elseif fields[i] == "refilled_at_ms" then
94+
refilledAtMilliseconds = tonumber(fields[i+1])
9195
end
9296
end
9397

94-
local refill = math.floor((now - refilledAt) / refillIntervalSeconds)
98+
local refill = math.floor((now - refilledAtMilliseconds) / (refillIntervalSeconds * 1000))
9599
count = math.min(count + refill, max)
96-
refilledAt = refilledAt + refill * refillIntervalSeconds
100+
refilledAtMilliseconds = refilledAtMilliseconds + refill * refillIntervalSeconds * 1000
97101

98102
if count < cost then
99103
return {0}
100104
end
101105

102106
count = count - cost
103107
local expiresInSeconds = (max - count) * refillIntervalSeconds
104-
redis.call("HSET", key, "count", count, "refilled_at", now)
108+
redis.call("HSET", key, "count", count, "refilled_at_ms", refilledAtMilliseconds)
105109
redis.call("EXPIRE", key, expiresInSeconds)
106110
return {1}
107111
```
@@ -128,13 +132,14 @@ export class TokenBucketRateLimit {
128132
}
129133

130134
public async consume(key: string, cost: number): Promise<boolean> {
135+
const key = `token_bucket.v1:${this.storageKey}:${refillIntervalSeconds}:${key}`;
131136
const result = await client.EVALSHA(SCRIPT_SHA, {
132-
keys: [`${this.storageKey}:${key}`],
137+
keys: [key],
133138
arguments: [
134139
this.max.toString(),
135140
this.refillIntervalSeconds.toString(),
136141
cost.toString(),
137-
Math.floor(Date.now() / 1000).toString()
142+
Date.now().toString()
138143
]
139144
});
140145
return Boolean(result[0]);
@@ -143,10 +148,8 @@ export class TokenBucketRateLimit {
143148
```
144149

145150
```ts
146-
// Bucket that has 10 tokens max and refills at a rate of 2 tokens/sec.
147-
// Ensure that the storage key is unique.
148-
const ratelimit = new TokenBucketRateLimit("global_ip", 10, 2);
149-
151+
// Bucket that has 10 tokens max and refills at a rate of 30 seconds/token
152+
const ratelimit = new TokenBucketRateLimit<string>("ip", 5, 30);
150153
const valid = await ratelimit.consume(ip, 1);
151154
if (!valid) {
152155
throw new Error("Too many requests");

0 commit comments

Comments
 (0)