@upstash/ratelimit 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.releaserc ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "branches": [
3
+ {
4
+ "name": "release"
5
+ },
6
+ {
7
+ "name": "main",
8
+ "channel": "next",
9
+ "prerelease": "next"
10
+ }
11
+ ],
12
+ "dryRun": false,
13
+ "ci": true
14
+ }
package/README.md CHANGED
@@ -1,12 +1,10 @@
1
- # Upstash Ratelimit
2
-
3
- An HTTP/REST based Redis client built on top of Upstash REST API.
4
- [Upstash REST API](https://docs.upstash.com/features/restapi).
1
+ # Upstash RateLimit
5
2
 
6
3
  [![Tests](https://github.com/upstash/ratelimit/actions/workflows/tests.yaml/badge.svg)](https://github.com/upstash/ratelimit/actions/workflows/tests.yaml)
7
4
  ![npm (scoped)](https://img.shields.io/npm/v/@upstash/ratelimit)
8
5
 
9
- It is the only connectionless (HTTP based) ratelimiter and designed for:
6
+ It is the only connectionless (HTTP based) rate limiting library and designed
7
+ for:
10
8
 
11
9
  - Serverless functions (AWS Lambda ...)
12
10
  - Cloudflare Workers
@@ -18,6 +16,7 @@ It is the only connectionless (HTTP based) ratelimiter and designed for:
18
16
 
19
17
  <!-- toc -->
20
18
 
19
+ - [Docs](#docs)
21
20
  - [Quick Start](#quick-start)
22
21
  - [Install](#install)
23
22
  - [npm](#npm)
@@ -25,8 +24,10 @@ It is the only connectionless (HTTP based) ratelimiter and designed for:
25
24
  - [Create database](#create-database)
26
25
  - [Use it](#use-it)
27
26
  - [Block until ready](#block-until-ready)
28
- - [Globally replicated ratelimiting](#globally-replicated-ratelimiting)
27
+ - [Ephemeral Cache](#ephemeral-cache)
28
+ - [MultiRegion replicated ratelimiting](#multiregion-replicated-ratelimiting)
29
29
  - [Usage](#usage)
30
+ - [Asynchronous synchronization between databases](#asynchronous-synchronization-between-databases)
30
31
  - [Example](#example)
31
32
  - [Ratelimiting algorithms](#ratelimiting-algorithms)
32
33
  - [Fixed Window](#fixed-window)
@@ -48,6 +49,10 @@ It is the only connectionless (HTTP based) ratelimiter and designed for:
48
49
 
49
50
  <!-- tocstop -->
50
51
 
52
+ ## Docs
53
+
54
+ [doc.deno.land](https://doc.deno.land/https://deno.land/x/upstash_ratelimit/src/mod.ts)
55
+
51
56
  ## Quick Start
52
57
 
53
58
  ### Install
@@ -176,11 +181,37 @@ doExpensiveCalculation();
176
181
  return "Here you go!";
177
182
  ```
178
183
 
179
- ## MultiRegionly replicated ratelimiting
184
+ ### Ephemeral Cache
185
+
186
+ For extreme load or denial of service attacks, it might be too expensive to call
187
+ redis for every incoming request, just to find out it should be blocked because
188
+ they have exceeded the limit.
189
+
190
+ You can use an ephemeral in memory cache by passing the `ephemeralCache` option:
191
+
192
+ ```ts
193
+ const cache = new Map(); // must be outside of your serverless function handler
194
+
195
+ // ...
196
+
197
+ const ratelimit = new Ratelimit({
198
+ // ...
199
+ ephemeralCache: cache,
200
+ });
201
+ ```
202
+
203
+ If enabled, the ratelimiter will keep a global cache of identifiers and their
204
+ reset timestamps, that have exhausted their ratelimit. In serverless
205
+ environments this is only possible if you create the cache or ratelimiter
206
+ instance outside of your handler function. While the function is still hot, the
207
+ ratelimiter can block requests without having to request data from redis, thus
208
+ saving time and money.
209
+
210
+ ## MultiRegion replicated ratelimiting
180
211
 
181
- Using a single redis instance has the downside of providing low latencies to the
182
- part of your userbase closest to the deployed db. That's why we also built
183
- `MultiRegionRatelimit` which replicates the state across multiple redis
212
+ Using a single redis instance has the downside of providing low latencies only
213
+ to the part of your userbase closest to the deployed db. That's why we also
214
+ built `MultiRegionRatelimit` which replicates the state across multiple redis
184
215
  databases as well as offering lower latencies to more of your users.
185
216
 
186
217
  `MultiRegionRatelimit` does this by checking the current limit in the closest db
@@ -247,7 +278,7 @@ context.waitUntil(pending);
247
278
  ### Example
248
279
 
249
280
  Let's assume you have customers in the US and Europe. In this case you can
250
- create 2 regional redis databases on [Upastash](https://console.upstash.com) and
281
+ create 2 regional redis databases on [Upstash](https://console.upstash.com) and
251
282
  your users will enjoy the latency of whichever db is closest to them.
252
283
 
253
284
  ## Ratelimiting algorithms
package/esm/cache.js ADDED
@@ -0,0 +1,28 @@
1
+ export class Cache {
2
+ constructor(cache) {
3
+ /**
4
+ * Stores identifier -> reset (in milliseconds)
5
+ */
6
+ Object.defineProperty(this, "cache", {
7
+ enumerable: true,
8
+ configurable: true,
9
+ writable: true,
10
+ value: void 0
11
+ });
12
+ this.cache = cache;
13
+ }
14
+ isBlocked(identifier) {
15
+ if (!this.cache.has(identifier)) {
16
+ return { blocked: false, reset: 0 };
17
+ }
18
+ const reset = this.cache.get(identifier);
19
+ if (reset < Date.now()) {
20
+ this.cache.delete(identifier);
21
+ return { blocked: false, reset: 0 };
22
+ }
23
+ return { blocked: true, reset: reset };
24
+ }
25
+ blockUntil(identifier, reset) {
26
+ this.cache.set(identifier, reset);
27
+ }
28
+ }
package/esm/multi.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { ms } from "./duration.js";
2
2
  import { Ratelimit } from "./ratelimit.js";
3
+ import { Cache } from "./cache.js";
3
4
  /**
4
5
  * Ratelimiter using serverless redis from https://upstash.com/
5
6
  *
@@ -23,7 +24,12 @@ export class MultiRegionRatelimit extends Ratelimit {
23
24
  super({
24
25
  prefix: config.prefix,
25
26
  limiter: config.limiter,
26
- ctx: { redis: config.redis },
27
+ ctx: {
28
+ redis: config.redis,
29
+ cache: config.ephermeralCache
30
+ ? new Cache(config.ephermeralCache)
31
+ : undefined,
32
+ },
27
33
  });
28
34
  }
29
35
  /**
@@ -70,6 +76,18 @@ export class MultiRegionRatelimit extends Ratelimit {
70
76
  return members
71
77
  `;
72
78
  return async function (ctx, identifier) {
79
+ if (ctx.cache) {
80
+ const { blocked, reset } = ctx.cache.isBlocked(identifier);
81
+ if (blocked) {
82
+ return {
83
+ success: false,
84
+ limit: tokens,
85
+ remaining: 0,
86
+ reset: reset,
87
+ pending: Promise.resolve(),
88
+ };
89
+ }
90
+ }
73
91
  const requestID = crypto.randomUUID();
74
92
  const bucket = Math.floor(Date.now() / windowDuration);
75
93
  const key = [identifier, bucket].join(":");
@@ -108,11 +126,16 @@ export class MultiRegionRatelimit extends Ratelimit {
108
126
  /**
109
127
  * Do not await sync. This should not run in the critical path.
110
128
  */
129
+ const success = remaining > 0;
130
+ const reset = (bucket + 1) * windowDuration;
131
+ if (ctx.cache && !success) {
132
+ ctx.cache.blockUntil(identifier, reset);
133
+ }
111
134
  return {
112
- success: remaining > 0,
135
+ success,
113
136
  limit: tokens,
114
137
  remaining,
115
- reset: (bucket + 1) * windowDuration,
138
+ reset,
116
139
  pending: sync(),
117
140
  };
118
141
  };
@@ -173,6 +196,18 @@ export class MultiRegionRatelimit extends Ratelimit {
173
196
  `;
174
197
  const windowDuration = ms(window);
175
198
  return async function (ctx, identifier) {
199
+ if (ctx.cache) {
200
+ const { blocked, reset } = ctx.cache.isBlocked(identifier);
201
+ if (blocked) {
202
+ return {
203
+ success: false,
204
+ limit: tokens,
205
+ remaining: 0,
206
+ reset: reset,
207
+ pending: Promise.resolve(),
208
+ };
209
+ }
210
+ }
176
211
  const requestID = crypto.randomUUID();
177
212
  const now = Date.now();
178
213
  const currentWindow = Math.floor(now / windowSize);
@@ -213,14 +248,16 @@ export class MultiRegionRatelimit extends Ratelimit {
213
248
  await db.redis.sadd(currentKey, ...allIDs);
214
249
  }
215
250
  }
216
- /**
217
- * Do not await sync. This should not run in the critical path.
218
- */
251
+ const success = remaining > 0;
252
+ const reset = (currentWindow + 1) * windowDuration;
253
+ if (ctx.cache && !success) {
254
+ ctx.cache.blockUntil(identifier, reset);
255
+ }
219
256
  return {
220
- success: remaining > 0,
257
+ success,
221
258
  limit: tokens,
222
259
  remaining,
223
- reset: (currentWindow + 1) * windowDuration,
260
+ reset,
224
261
  pending: sync(),
225
262
  };
226
263
  };
package/esm/ratelimit.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { Cache } from "./cache.js";
1
2
  /**
2
3
  * Ratelimiter using serverless redis from https://upstash.com/
3
4
  *
@@ -125,5 +126,11 @@ export class Ratelimit {
125
126
  this.ctx = config.ctx;
126
127
  this.limiter = config.limiter;
127
128
  this.prefix = config.prefix ?? "@upstash/ratelimit";
129
+ if (config.ephermeralCache instanceof Map) {
130
+ this.ctx.cache = new Cache(config.ephermeralCache);
131
+ }
132
+ else if (typeof config.ephermeralCache === "undefined") {
133
+ this.ctx.cache = new Cache(new Map());
134
+ }
128
135
  }
129
136
  }
package/esm/single.js CHANGED
@@ -23,7 +23,10 @@ export class RegionRatelimit extends Ratelimit {
23
23
  super({
24
24
  prefix: config.prefix,
25
25
  limiter: config.limiter,
26
- ctx: { redis: config.redis },
26
+ ctx: {
27
+ redis: config.redis,
28
+ },
29
+ ephermeralCache: config.ephermeralCache,
27
30
  });
28
31
  }
29
32
  /**
@@ -70,12 +73,29 @@ export class RegionRatelimit extends Ratelimit {
70
73
  return async function (ctx, identifier) {
71
74
  const bucket = Math.floor(Date.now() / windowDuration);
72
75
  const key = [identifier, bucket].join(":");
76
+ if (ctx.cache) {
77
+ const { blocked, reset } = ctx.cache.isBlocked(identifier);
78
+ if (blocked) {
79
+ return {
80
+ success: false,
81
+ limit: tokens,
82
+ remaining: 0,
83
+ reset: reset,
84
+ pending: Promise.resolve(),
85
+ };
86
+ }
87
+ }
73
88
  const usedTokensAfterUpdate = (await ctx.redis.eval(script, [key], [windowDuration]));
89
+ const success = usedTokensAfterUpdate <= tokens;
90
+ const reset = (bucket + 1) * windowDuration;
91
+ if (ctx.cache && !success) {
92
+ ctx.cache.blockUntil(identifier, reset);
93
+ }
74
94
  return {
75
- success: usedTokensAfterUpdate <= tokens,
95
+ success,
76
96
  limit: tokens,
77
97
  remaining: tokens - usedTokensAfterUpdate,
78
- reset: (bucket + 1) * windowDuration,
98
+ reset,
79
99
  pending: Promise.resolve(),
80
100
  };
81
101
  };
@@ -142,12 +162,29 @@ export class RegionRatelimit extends Ratelimit {
142
162
  const currentKey = [identifier, currentWindow].join(":");
143
163
  const previousWindow = currentWindow - windowSize;
144
164
  const previousKey = [identifier, previousWindow].join(":");
165
+ if (ctx.cache) {
166
+ const { blocked, reset } = ctx.cache.isBlocked(identifier);
167
+ if (blocked) {
168
+ return {
169
+ success: false,
170
+ limit: tokens,
171
+ remaining: 0,
172
+ reset: reset,
173
+ pending: Promise.resolve(),
174
+ };
175
+ }
176
+ }
145
177
  const remaining = (await ctx.redis.eval(script, [currentKey, previousKey], [tokens, now, windowSize]));
178
+ const success = remaining > 0;
179
+ const reset = (currentWindow + 1) * windowSize;
180
+ if (ctx.cache && !success) {
181
+ ctx.cache.blockUntil(identifier, reset);
182
+ }
146
183
  return {
147
- success: remaining > 0,
184
+ success,
148
185
  limit: tokens,
149
186
  remaining,
150
- reset: (currentWindow + 1) * windowSize,
187
+ reset,
151
188
  pending: Promise.resolve(),
152
189
  };
153
190
  };
@@ -164,8 +201,6 @@ export class RegionRatelimit extends Ratelimit {
164
201
  * rate.
165
202
  * - Allows to set a higher initial burst limit by setting `maxTokens` higher
166
203
  * than `refillRate`
167
- *
168
- * **Usage of Upstash Redis requests:**
169
204
  */
170
205
  static tokenBucket(
171
206
  /**
@@ -225,11 +260,27 @@ export class RegionRatelimit extends Ratelimit {
225
260
  `;
226
261
  const intervalDuration = ms(interval);
227
262
  return async function (ctx, identifier) {
263
+ if (ctx.cache) {
264
+ const { blocked, reset } = ctx.cache.isBlocked(identifier);
265
+ if (blocked) {
266
+ return {
267
+ success: false,
268
+ limit: maxTokens,
269
+ remaining: 0,
270
+ reset: reset,
271
+ pending: Promise.resolve(),
272
+ };
273
+ }
274
+ }
228
275
  const now = Date.now();
229
276
  const key = [identifier, Math.floor(now / intervalDuration)].join(":");
230
277
  const [remaining, reset] = (await ctx.redis.eval(script, [key], [maxTokens, intervalDuration, refillRate, now]));
278
+ const success = remaining > 0;
279
+ if (ctx.cache && !success) {
280
+ ctx.cache.blockUntil(identifier, reset);
281
+ }
231
282
  return {
232
- success: remaining > 0,
283
+ success,
233
284
  limit: maxTokens,
234
285
  remaining,
235
286
  reset,
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "main": "./script/mod.js",
4
4
  "types": "./types/mod.d.ts",
5
5
  "name": "@upstash/ratelimit",
6
- "version": "v0.1.3",
6
+ "version": "v0.1.4",
7
7
  "description": "A serverless ratelimiter built on top of Upstash REST API.",
8
8
  "repository": {
9
9
  "type": "git",
@@ -29,7 +29,7 @@
29
29
  "size-limit": "latest"
30
30
  },
31
31
  "peerDependencies": {
32
- "@upstash/redis": "^1.3.4"
32
+ "@upstash/redis": "^1.4.0"
33
33
  },
34
34
  "size-limit": [
35
35
  {
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Cache = void 0;
4
+ class Cache {
5
+ constructor(cache) {
6
+ /**
7
+ * Stores identifier -> reset (in milliseconds)
8
+ */
9
+ Object.defineProperty(this, "cache", {
10
+ enumerable: true,
11
+ configurable: true,
12
+ writable: true,
13
+ value: void 0
14
+ });
15
+ this.cache = cache;
16
+ }
17
+ isBlocked(identifier) {
18
+ if (!this.cache.has(identifier)) {
19
+ return { blocked: false, reset: 0 };
20
+ }
21
+ const reset = this.cache.get(identifier);
22
+ if (reset < Date.now()) {
23
+ this.cache.delete(identifier);
24
+ return { blocked: false, reset: 0 };
25
+ }
26
+ return { blocked: true, reset: reset };
27
+ }
28
+ blockUntil(identifier, reset) {
29
+ this.cache.set(identifier, reset);
30
+ }
31
+ }
32
+ exports.Cache = Cache;
package/script/multi.js CHANGED
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MultiRegionRatelimit = void 0;
4
4
  const duration_js_1 = require("./duration.js");
5
5
  const ratelimit_js_1 = require("./ratelimit.js");
6
+ const cache_js_1 = require("./cache.js");
6
7
  /**
7
8
  * Ratelimiter using serverless redis from https://upstash.com/
8
9
  *
@@ -26,7 +27,12 @@ class MultiRegionRatelimit extends ratelimit_js_1.Ratelimit {
26
27
  super({
27
28
  prefix: config.prefix,
28
29
  limiter: config.limiter,
29
- ctx: { redis: config.redis },
30
+ ctx: {
31
+ redis: config.redis,
32
+ cache: config.ephermeralCache
33
+ ? new cache_js_1.Cache(config.ephermeralCache)
34
+ : undefined,
35
+ },
30
36
  });
31
37
  }
32
38
  /**
@@ -73,6 +79,18 @@ class MultiRegionRatelimit extends ratelimit_js_1.Ratelimit {
73
79
  return members
74
80
  `;
75
81
  return async function (ctx, identifier) {
82
+ if (ctx.cache) {
83
+ const { blocked, reset } = ctx.cache.isBlocked(identifier);
84
+ if (blocked) {
85
+ return {
86
+ success: false,
87
+ limit: tokens,
88
+ remaining: 0,
89
+ reset: reset,
90
+ pending: Promise.resolve(),
91
+ };
92
+ }
93
+ }
76
94
  const requestID = crypto.randomUUID();
77
95
  const bucket = Math.floor(Date.now() / windowDuration);
78
96
  const key = [identifier, bucket].join(":");
@@ -111,11 +129,16 @@ class MultiRegionRatelimit extends ratelimit_js_1.Ratelimit {
111
129
  /**
112
130
  * Do not await sync. This should not run in the critical path.
113
131
  */
132
+ const success = remaining > 0;
133
+ const reset = (bucket + 1) * windowDuration;
134
+ if (ctx.cache && !success) {
135
+ ctx.cache.blockUntil(identifier, reset);
136
+ }
114
137
  return {
115
- success: remaining > 0,
138
+ success,
116
139
  limit: tokens,
117
140
  remaining,
118
- reset: (bucket + 1) * windowDuration,
141
+ reset,
119
142
  pending: sync(),
120
143
  };
121
144
  };
@@ -176,6 +199,18 @@ class MultiRegionRatelimit extends ratelimit_js_1.Ratelimit {
176
199
  `;
177
200
  const windowDuration = (0, duration_js_1.ms)(window);
178
201
  return async function (ctx, identifier) {
202
+ if (ctx.cache) {
203
+ const { blocked, reset } = ctx.cache.isBlocked(identifier);
204
+ if (blocked) {
205
+ return {
206
+ success: false,
207
+ limit: tokens,
208
+ remaining: 0,
209
+ reset: reset,
210
+ pending: Promise.resolve(),
211
+ };
212
+ }
213
+ }
179
214
  const requestID = crypto.randomUUID();
180
215
  const now = Date.now();
181
216
  const currentWindow = Math.floor(now / windowSize);
@@ -216,14 +251,16 @@ class MultiRegionRatelimit extends ratelimit_js_1.Ratelimit {
216
251
  await db.redis.sadd(currentKey, ...allIDs);
217
252
  }
218
253
  }
219
- /**
220
- * Do not await sync. This should not run in the critical path.
221
- */
254
+ const success = remaining > 0;
255
+ const reset = (currentWindow + 1) * windowDuration;
256
+ if (ctx.cache && !success) {
257
+ ctx.cache.blockUntil(identifier, reset);
258
+ }
222
259
  return {
223
- success: remaining > 0,
260
+ success,
224
261
  limit: tokens,
225
262
  remaining,
226
- reset: (currentWindow + 1) * windowDuration,
263
+ reset,
227
264
  pending: sync(),
228
265
  };
229
266
  };
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Ratelimit = void 0;
4
+ const cache_js_1 = require("./cache.js");
4
5
  /**
5
6
  * Ratelimiter using serverless redis from https://upstash.com/
6
7
  *
@@ -128,6 +129,12 @@ class Ratelimit {
128
129
  this.ctx = config.ctx;
129
130
  this.limiter = config.limiter;
130
131
  this.prefix = config.prefix ?? "@upstash/ratelimit";
132
+ if (config.ephermeralCache instanceof Map) {
133
+ this.ctx.cache = new cache_js_1.Cache(config.ephermeralCache);
134
+ }
135
+ else if (typeof config.ephermeralCache === "undefined") {
136
+ this.ctx.cache = new cache_js_1.Cache(new Map());
137
+ }
131
138
  }
132
139
  }
133
140
  exports.Ratelimit = Ratelimit;
package/script/single.js CHANGED
@@ -26,7 +26,10 @@ class RegionRatelimit extends ratelimit_js_1.Ratelimit {
26
26
  super({
27
27
  prefix: config.prefix,
28
28
  limiter: config.limiter,
29
- ctx: { redis: config.redis },
29
+ ctx: {
30
+ redis: config.redis,
31
+ },
32
+ ephermeralCache: config.ephermeralCache,
30
33
  });
31
34
  }
32
35
  /**
@@ -73,12 +76,29 @@ class RegionRatelimit extends ratelimit_js_1.Ratelimit {
73
76
  return async function (ctx, identifier) {
74
77
  const bucket = Math.floor(Date.now() / windowDuration);
75
78
  const key = [identifier, bucket].join(":");
79
+ if (ctx.cache) {
80
+ const { blocked, reset } = ctx.cache.isBlocked(identifier);
81
+ if (blocked) {
82
+ return {
83
+ success: false,
84
+ limit: tokens,
85
+ remaining: 0,
86
+ reset: reset,
87
+ pending: Promise.resolve(),
88
+ };
89
+ }
90
+ }
76
91
  const usedTokensAfterUpdate = (await ctx.redis.eval(script, [key], [windowDuration]));
92
+ const success = usedTokensAfterUpdate <= tokens;
93
+ const reset = (bucket + 1) * windowDuration;
94
+ if (ctx.cache && !success) {
95
+ ctx.cache.blockUntil(identifier, reset);
96
+ }
77
97
  return {
78
- success: usedTokensAfterUpdate <= tokens,
98
+ success,
79
99
  limit: tokens,
80
100
  remaining: tokens - usedTokensAfterUpdate,
81
- reset: (bucket + 1) * windowDuration,
101
+ reset,
82
102
  pending: Promise.resolve(),
83
103
  };
84
104
  };
@@ -145,12 +165,29 @@ class RegionRatelimit extends ratelimit_js_1.Ratelimit {
145
165
  const currentKey = [identifier, currentWindow].join(":");
146
166
  const previousWindow = currentWindow - windowSize;
147
167
  const previousKey = [identifier, previousWindow].join(":");
168
+ if (ctx.cache) {
169
+ const { blocked, reset } = ctx.cache.isBlocked(identifier);
170
+ if (blocked) {
171
+ return {
172
+ success: false,
173
+ limit: tokens,
174
+ remaining: 0,
175
+ reset: reset,
176
+ pending: Promise.resolve(),
177
+ };
178
+ }
179
+ }
148
180
  const remaining = (await ctx.redis.eval(script, [currentKey, previousKey], [tokens, now, windowSize]));
181
+ const success = remaining > 0;
182
+ const reset = (currentWindow + 1) * windowSize;
183
+ if (ctx.cache && !success) {
184
+ ctx.cache.blockUntil(identifier, reset);
185
+ }
149
186
  return {
150
- success: remaining > 0,
187
+ success,
151
188
  limit: tokens,
152
189
  remaining,
153
- reset: (currentWindow + 1) * windowSize,
190
+ reset,
154
191
  pending: Promise.resolve(),
155
192
  };
156
193
  };
@@ -167,8 +204,6 @@ class RegionRatelimit extends ratelimit_js_1.Ratelimit {
167
204
  * rate.
168
205
  * - Allows to set a higher initial burst limit by setting `maxTokens` higher
169
206
  * than `refillRate`
170
- *
171
- * **Usage of Upstash Redis requests:**
172
207
  */
173
208
  static tokenBucket(
174
209
  /**
@@ -228,11 +263,27 @@ class RegionRatelimit extends ratelimit_js_1.Ratelimit {
228
263
  `;
229
264
  const intervalDuration = (0, duration_js_1.ms)(interval);
230
265
  return async function (ctx, identifier) {
266
+ if (ctx.cache) {
267
+ const { blocked, reset } = ctx.cache.isBlocked(identifier);
268
+ if (blocked) {
269
+ return {
270
+ success: false,
271
+ limit: maxTokens,
272
+ remaining: 0,
273
+ reset: reset,
274
+ pending: Promise.resolve(),
275
+ };
276
+ }
277
+ }
231
278
  const now = Date.now();
232
279
  const key = [identifier, Math.floor(now / intervalDuration)].join(":");
233
280
  const [remaining, reset] = (await ctx.redis.eval(script, [key], [maxTokens, intervalDuration, refillRate, now]));
281
+ const success = remaining > 0;
282
+ if (ctx.cache && !success) {
283
+ ctx.cache.blockUntil(identifier, reset);
284
+ }
234
285
  return {
235
- success: remaining > 0,
286
+ success,
236
287
  limit: maxTokens,
237
288
  remaining,
238
289
  reset,
@@ -0,0 +1,13 @@
1
+ import { EphermeralCache } from "./types.js";
2
+ export declare class Cache implements EphermeralCache {
3
+ /**
4
+ * Stores identifier -> reset (in milliseconds)
5
+ */
6
+ private readonly cache;
7
+ constructor(cache: Map<string, number>);
8
+ isBlocked(identifier: string): {
9
+ blocked: boolean;
10
+ reset: number;
11
+ };
12
+ blockUntil(identifier: string, reset: number): void;
13
+ }
package/types/multi.d.ts CHANGED
@@ -22,6 +22,23 @@ export declare type MultiRegionRatelimitConfig = {
22
22
  * @default `@upstash/ratelimit`
23
23
  */
24
24
  prefix?: string;
25
+ /**
26
+ * If enabled, the ratelimiter will keep a global cache of identifiers, that have
27
+ * exhausted their ratelimit. In serverless environments this is only possible if
28
+ * you create the ratelimiter instance outside of your handler function. While the
29
+ * function is still hot, the ratelimiter can block requests without having to
30
+ * request data from redis, thus saving time and money.
31
+ *
32
+ * Whenever an identifier has exceeded its limit, the ratelimiter will add it to an
33
+ * internal list together with its reset timestamp. If the same identifier makes a
34
+ * new request before it is reset, we can immediately reject it.
35
+ *
36
+ * Set to `false` to disable.
37
+ *
38
+ * If left undefined, a map is created automatically, but it can only work
39
+ * if the map or th ratelimit instance is created outside your serverless function handler.
40
+ */
41
+ ephermeralCache?: Map<string, number> | false;
25
42
  };
26
43
  /**
27
44
  * Ratelimiter using serverless redis from https://upstash.com/
@@ -18,6 +18,23 @@ export declare type RatelimitConfig<TContext> = {
18
18
  * @default `@upstash/ratelimit`
19
19
  */
20
20
  prefix?: string;
21
+ /**
22
+ * If enabled, the ratelimiter will keep a global cache of identifiers, that have
23
+ * exhausted their ratelimit. In serverless environments this is only possible if
24
+ * you create the ratelimiter instance outside of your handler function. While the
25
+ * function is still hot, the ratelimiter can block requests without having to
26
+ * request data from redis, thus saving time and money.
27
+ *
28
+ * Whenever an identifier has exceeded its limit, the ratelimiter will add it to an
29
+ * internal list together with its reset timestamp. If the same identifier makes a
30
+ * new request before it is reset, we can immediately reject it.
31
+ *
32
+ * Set to `false` to disable.
33
+ *
34
+ * If left undefined, a map is created automatically, but it can only work
35
+ * if the map or the ratelimit instance is created outside your serverless function handler.
36
+ */
37
+ ephermeralCache?: Map<string, number> | false;
21
38
  };
22
39
  /**
23
40
  * Ratelimiter using serverless redis from https://upstash.com/
package/types/single.d.ts CHANGED
@@ -25,6 +25,23 @@ export declare type RegionRatelimitConfig = {
25
25
  * @default `@upstash/ratelimit`
26
26
  */
27
27
  prefix?: string;
28
+ /**
29
+ * If enabled, the ratelimiter will keep a global cache of identifiers, that have
30
+ * exhausted their ratelimit. In serverless environments this is only possible if
31
+ * you create the ratelimiter instance outside of your handler function. While the
32
+ * function is still hot, the ratelimiter can block requests without having to
33
+ * request data from redis, thus saving time and money.
34
+ *
35
+ * Whenever an identifier has exceeded its limit, the ratelimiter will add it to an
36
+ * internal list together with its reset timestamp. If the same identifier makes a
37
+ * new request before it is reset, we can immediately reject it.
38
+ *
39
+ * Set to `false` to disable.
40
+ *
41
+ * If left undefined, a map is created automatically, but it can only work
42
+ * if the map or the ratelimit instance is created outside your serverless function handler.
43
+ */
44
+ ephermeralCache?: Map<string, number> | false;
28
45
  };
29
46
  /**
30
47
  * Ratelimiter using serverless redis from https://upstash.com/
@@ -110,8 +127,6 @@ export declare class RegionRatelimit extends Ratelimit<RegionContext> {
110
127
  * rate.
111
128
  * - Allows to set a higher initial burst limit by setting `maxTokens` higher
112
129
  * than `refillRate`
113
- *
114
- * **Usage of Upstash Redis requests:**
115
130
  */
116
131
  static tokenBucket(
117
132
  /**
package/types/types.d.ts CHANGED
@@ -2,11 +2,23 @@ export interface Redis {
2
2
  eval: (script: string, keys: string[], values: unknown[]) => Promise<unknown>;
3
3
  sadd: (key: string, ...members: string[]) => Promise<number>;
4
4
  }
5
+ /**
6
+ * EphermeralCache is used to block certain identifiers right away in case they have already exceedd the ratelimit.
7
+ */
8
+ export interface EphermeralCache {
9
+ isBlocked: (identifier: string) => {
10
+ blocked: boolean;
11
+ reset: number;
12
+ };
13
+ blockUntil: (identifier: string, reset: number) => void;
14
+ }
5
15
  export declare type RegionContext = {
6
16
  redis: Redis;
17
+ cache?: EphermeralCache;
7
18
  };
8
19
  export declare type MultiRegionContext = {
9
20
  redis: Redis[];
21
+ cache?: EphermeralCache;
10
22
  };
11
23
  export declare type Context = RegionContext | MultiRegionContext;
12
24
  export declare type RatelimitResponse = {
@@ -50,4 +62,6 @@ export declare type RatelimitResponse = {
50
62
  */
51
63
  pending: Promise<unknown>;
52
64
  };
53
- export declare type Algorithm<TContext> = (ctx: TContext, identifier: string) => Promise<RatelimitResponse>;
65
+ export declare type Algorithm<TContext> = (ctx: TContext, identifier: string, opts?: {
66
+ cache?: EphermeralCache;
67
+ }) => Promise<RatelimitResponse>;