Skip to content

Commit

Permalink
add redis throttler guide
Browse files Browse the repository at this point in the history
  • Loading branch information
pilcrowonpaper committed Oct 5, 2024
1 parent d882063 commit 6cbaed7
Showing 1 changed file with 85 additions and 3 deletions.
88 changes: 85 additions & 3 deletions pages/rate-limit/throttling.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ After each failed attempt, the user has to wait longer before their next attempt

## Memory storage

`timeoutSeconds` holds the number of seconds to lock out the user for.

```ts
export class Throttler<_Key> {
public timeoutSeconds: number[];
Expand Down Expand Up @@ -50,15 +52,95 @@ interface ThrottlingCounter {
}
```

Here, on each failed sign in attempt, the lockout gets extended with a max of 5 minutes.
Here, on each failed sign in attempt, the lockout time gets extended with a max of 5 minutes.

```ts
const throttler = new Throttler<number>([1, 2, 4, 8, 16, 30, 60, 180, 300]);

if (!throttler.consume(userId)) {
throw new Error("Too many requests");
}
const validPassword = verifyPassword(password);
if (!validPassword) {
throw new Error("Invalid password");
}
throttler.reset(user.id);
```

## Redis

We'll use Lua scripts to ensure queries are atomic. `timeoutSeconds` holds the number of seconds to lock out the user for.

```lua
-- Returns 1 if allowed, 0 if not
local key = KEYS[1]
local now = tonumber(ARGV[1])

local timeoutSeconds = {1, 2, 4, 8, 16, 30, 60, 180, 300}

local fields = redis.call("HGETALL", key)
if #fields == 0 then
redis.call("HSET", key, "index", 1, "updated_at", now)
return {1}
end
local index = 0
local updatedAt = 0
for i = 1, #fields, 2 do
if fields[i] == "index" then
index = tonumber(fields[i+1])
elseif fields[i] == "updated_at" then
updatedAt = tonumber(fields[i+1])
end
end
local allowed = now - updatedAt >= timeoutSeconds[index]
if not allowed then
return {0}
end
index = math.min(index + 1, #timeoutSeconds)
redis.call("HSET", key, "index", index, "updated_at", now)
return {1}
```

Load the script and retrieve the script hash.

```ts
const SCRIPT_SHA = await client.scriptLoad(script);
```

Reference the script with the hash.

```ts
export class Throttler {
private storageKey: string;

constructor(storageKey: string) {
this.storageKey = storageKey;
}

public async consume(key: string): Promise<boolean> {
const result = await client.EVALSHA(SCRIPT_SHA, {
keys: [`${this.storageKey}:${key}`],
arguments: [Math.floor(Date.now() / 1000).toString()]
});
return Boolean(result[0]);
}

public async reset(key: string): Promise<void> {
await client.DEL(key);
}
}
```

Here, on each failed sign in attempt, the lockout time gets extended.

```ts
const throttler = new Throttler<number>([0, 1, 2, 4, 8, 16, 30, 60, 180, 300]);
const throttler = new Throttler<number>("login_throttler");

if (!throttler.consume(userId)) {
throw new Error("Too many requests");
}
if (!verifyPassword(password)) {
const validPassword = verifyPassword(password);
if (!validPassword) {
throw new Error("Invalid password");
}
throttler.reset(user.id);
Expand Down

0 comments on commit 6cbaed7

Please sign in to comment.