@upstash/ratelimit 2.0.6 → 2.0.7

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/dist/index.d.mts CHANGED
@@ -12,7 +12,7 @@ type EphemeralCache = {
12
12
  blockUntil: (identifier: string, reset: number) => void;
13
13
  set: (key: string, value: number) => void;
14
14
  get: (key: string) => number | null;
15
- incr: (key: string) => number;
15
+ incr: (key: string, incrementAmount?: number) => number;
16
16
  pop: (key: string) => void;
17
17
  empty: () => void;
18
18
  size: () => number;
@@ -497,7 +497,7 @@ declare class MultiRegionRatelimit extends Ratelimit<MultiRegionContext> {
497
497
  window: Duration): Algorithm<MultiRegionContext>;
498
498
  }
499
499
 
500
- type Redis = Pick<Redis$1, "get" | "set">;
500
+ type Redis = Pick<Redis$1, "evalsha" | "get" | "set">;
501
501
  type RegionRatelimitConfig = {
502
502
  /**
503
503
  * Instance of `@upstash/redis`
package/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@ type EphemeralCache = {
12
12
  blockUntil: (identifier: string, reset: number) => void;
13
13
  set: (key: string, value: number) => void;
14
14
  get: (key: string) => number | null;
15
- incr: (key: string) => number;
15
+ incr: (key: string, incrementAmount?: number) => number;
16
16
  pop: (key: string) => void;
17
17
  empty: () => void;
18
18
  size: () => number;
@@ -497,7 +497,7 @@ declare class MultiRegionRatelimit extends Ratelimit<MultiRegionContext> {
497
497
  window: Duration): Algorithm<MultiRegionContext>;
498
498
  }
499
499
 
500
- type Redis = Pick<Redis$1, "get" | "set">;
500
+ type Redis = Pick<Redis$1, "evalsha" | "get" | "set">;
501
501
  type RegionRatelimitConfig = {
502
502
  /**
503
503
  * Instance of `@upstash/redis`
package/dist/index.js CHANGED
@@ -115,9 +115,9 @@ var Cache = class {
115
115
  get(key) {
116
116
  return this.cache.get(key) || null;
117
117
  }
118
- incr(key) {
118
+ incr(key, incrementAmount = 1) {
119
119
  let value = this.cache.get(key) ?? 0;
120
- value += 1;
120
+ value += incrementAmount;
121
121
  this.cache.set(key, value);
122
122
  return value;
123
123
  }
@@ -205,7 +205,7 @@ var slidingWindowLimitScript = `
205
205
  local tokens = tonumber(ARGV[1]) -- tokens per window
206
206
  local now = ARGV[2] -- current timestamp in milliseconds
207
207
  local window = ARGV[3] -- interval in milliseconds
208
- local incrementBy = ARGV[4] -- increment rate per request at a given value, default is 1
208
+ local incrementBy = tonumber(ARGV[4]) -- increment rate per request at a given value, default is 1
209
209
 
210
210
  local requestsInCurrentWindow = redis.call("GET", currentKey)
211
211
  if requestsInCurrentWindow == false then
@@ -219,12 +219,14 @@ var slidingWindowLimitScript = `
219
219
  local percentageInCurrent = ( now % window ) / window
220
220
  -- weighted requests to consider from the previous window
221
221
  requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
222
- if requestsInPreviousWindow + requestsInCurrentWindow >= tokens then
222
+
223
+ -- Only check limit if not refunding (negative rate)
224
+ if incrementBy > 0 and requestsInPreviousWindow + requestsInCurrentWindow >= tokens then
223
225
  return -1
224
226
  end
225
227
 
226
228
  local newValue = redis.call("INCRBY", currentKey, incrementBy)
227
- if newValue == tonumber(incrementBy) then
229
+ if newValue == incrementBy then
228
230
  -- The first time this key is set, the value will be equal to incrementBy.
229
231
  -- So we only need the expire command once
230
232
  redis.call("PEXPIRE", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second
@@ -281,7 +283,8 @@ var tokenBucketLimitScript = `
281
283
  refilledAt = refilledAt + numRefills * interval
282
284
  end
283
285
 
284
- if tokens == 0 then
286
+ -- Only reject if tokens are 0 and we're consuming (not refunding)
287
+ if tokens == 0 and incrementBy > 0 then
285
288
  return {-1, refilledAt + interval}
286
289
  end
287
290
 
@@ -289,7 +292,10 @@ var tokenBucketLimitScript = `
289
292
  local expireAt = math.ceil(((maxTokens - remaining) / refillRate)) * interval
290
293
 
291
294
  redis.call("HSET", key, "refilledAt", refilledAt, "tokens", remaining)
292
- redis.call("PEXPIRE", key, expireAt)
295
+
296
+ if (expireAt > 0) then
297
+ redis.call("PEXPIRE", key, expireAt)
298
+ end
293
299
  return {remaining, refilledAt + interval}
294
300
  `;
295
301
  var tokenBucketIdentifierNotFound = -1;
@@ -377,7 +383,9 @@ var slidingWindowLimitScript2 = `
377
383
  end
378
384
 
379
385
  local percentageInCurrent = ( now % window) / window
380
- if requestsInPreviousWindow * (1 - percentageInCurrent ) + requestsInCurrentWindow >= tokens then
386
+
387
+ -- Only check limit if not refunding (negative rate)
388
+ if incrementBy > 0 and requestsInPreviousWindow * (1 - percentageInCurrent ) + requestsInCurrentWindow + incrementBy > tokens then
381
389
  return {currentFields, previousFields, false}
382
390
  end
383
391
 
@@ -455,7 +463,7 @@ var SCRIPTS = {
455
463
  slidingWindow: {
456
464
  limit: {
457
465
  script: slidingWindowLimitScript,
458
- hash: "e1391e429b699c780eb0480350cd5b7280fd9213"
466
+ hash: "9b7842963bd73721f1a3011650c23c0010848ee3"
459
467
  },
460
468
  getRemaining: {
461
469
  script: slidingWindowRemainingTokensScript,
@@ -465,7 +473,7 @@ var SCRIPTS = {
465
473
  tokenBucket: {
466
474
  limit: {
467
475
  script: tokenBucketLimitScript,
468
- hash: "5bece90aeef8189a8cfd28995b479529e270b3c6"
476
+ hash: "d1f857ebbdaeca90ccd2cd4eada61d7c8e5db1ca"
469
477
  },
470
478
  getRemaining: {
471
479
  script: tokenBucketRemainingTokensScript,
@@ -497,7 +505,7 @@ var SCRIPTS = {
497
505
  slidingWindow: {
498
506
  limit: {
499
507
  script: slidingWindowLimitScript2,
500
- hash: "cb4fdc2575056df7c6d422764df0de3a08d6753b"
508
+ hash: "1e7ca8dcd2d600a6d0124a67a57ea225ed62921b"
501
509
  },
502
510
  getRemaining: {
503
511
  script: slidingWindowRemainingTokensScript2,
@@ -973,7 +981,11 @@ var MultiRegionRatelimit = class extends Ratelimit {
973
981
  const windowDuration = ms(window);
974
982
  return () => ({
975
983
  async limit(ctx, identifier, rate) {
976
- if (ctx.cache) {
984
+ const requestId = randomId();
985
+ const bucket = Math.floor(Date.now() / windowDuration);
986
+ const key = [identifier, bucket].join(":");
987
+ const incrementBy = rate ?? 1;
988
+ if (ctx.cache && incrementBy > 0) {
977
989
  const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
978
990
  if (blocked) {
979
991
  return {
@@ -986,10 +998,6 @@ var MultiRegionRatelimit = class extends Ratelimit {
986
998
  };
987
999
  }
988
1000
  }
989
- const requestId = randomId();
990
- const bucket = Math.floor(Date.now() / windowDuration);
991
- const key = [identifier, bucket].join(":");
992
- const incrementBy = rate ? Math.max(1, rate) : 1;
993
1001
  const dbs = ctx.regionContexts.map((regionContext) => ({
994
1002
  redis: regionContext.redis,
995
1003
  request: safeEval(
@@ -1000,24 +1008,29 @@ var MultiRegionRatelimit = class extends Ratelimit {
1000
1008
  )
1001
1009
  }));
1002
1010
  const firstResponse = await Promise.any(dbs.map((s) => s.request));
1003
- const usedTokens = firstResponse.reduce((accTokens, usedToken, index) => {
1004
- let parsedToken = 0;
1005
- if (index % 2) {
1006
- parsedToken = Number.parseInt(usedToken);
1007
- }
1008
- return accTokens + parsedToken;
1009
- }, 0);
1011
+ const usedTokens = firstResponse.reduce(
1012
+ (accTokens, usedToken, index) => {
1013
+ let parsedToken = 0;
1014
+ if (index % 2) {
1015
+ parsedToken = Number.parseInt(usedToken);
1016
+ }
1017
+ return accTokens + parsedToken;
1018
+ },
1019
+ 0
1020
+ );
1010
1021
  const remaining = tokens - usedTokens;
1011
1022
  async function sync() {
1012
1023
  const individualIDs = await Promise.all(dbs.map((s) => s.request));
1013
- const allIDs = [...new Set(
1014
- individualIDs.flat().reduce((acc, curr, index) => {
1015
- if (index % 2 === 0) {
1016
- acc.push(curr);
1017
- }
1018
- return acc;
1019
- }, [])
1020
- ).values()];
1024
+ const allIDs = [
1025
+ ...new Set(
1026
+ individualIDs.flat().reduce((acc, curr, index) => {
1027
+ if (index % 2 === 0) {
1028
+ acc.push(curr);
1029
+ }
1030
+ return acc;
1031
+ }, [])
1032
+ ).values()
1033
+ ];
1021
1034
  for (const db of dbs) {
1022
1035
  const usedDbTokensRequest = await db.request;
1023
1036
  const usedDbTokens = usedDbTokensRequest.reduce(
@@ -1031,12 +1044,15 @@ var MultiRegionRatelimit = class extends Ratelimit {
1031
1044
  0
1032
1045
  );
1033
1046
  const dbIdsRequest = await db.request;
1034
- const dbIds = dbIdsRequest.reduce((ids, currentId, index) => {
1035
- if (index % 2 === 0) {
1036
- ids.push(currentId);
1037
- }
1038
- return ids;
1039
- }, []);
1047
+ const dbIds = dbIdsRequest.reduce(
1048
+ (ids, currentId, index) => {
1049
+ if (index % 2 === 0) {
1050
+ ids.push(currentId);
1051
+ }
1052
+ return ids;
1053
+ },
1054
+ []
1055
+ );
1040
1056
  if (usedDbTokens >= tokens) {
1041
1057
  continue;
1042
1058
  }
@@ -1049,10 +1065,14 @@ var MultiRegionRatelimit = class extends Ratelimit {
1049
1065
  }
1050
1066
  }
1051
1067
  }
1052
- const success = remaining > 0;
1068
+ const success = remaining >= 0;
1053
1069
  const reset = (bucket + 1) * windowDuration;
1054
- if (ctx.cache && !success) {
1055
- ctx.cache.blockUntil(identifier, reset);
1070
+ if (ctx.cache) {
1071
+ if (!success) {
1072
+ ctx.cache.blockUntil(identifier, reset);
1073
+ } else if (incrementBy < 0) {
1074
+ ctx.cache.pop(identifier);
1075
+ }
1056
1076
  }
1057
1077
  return {
1058
1078
  success,
@@ -1075,13 +1095,16 @@ var MultiRegionRatelimit = class extends Ratelimit {
1075
1095
  )
1076
1096
  }));
1077
1097
  const firstResponse = await Promise.any(dbs.map((s) => s.request));
1078
- const usedTokens = firstResponse.reduce((accTokens, usedToken, index) => {
1079
- let parsedToken = 0;
1080
- if (index % 2) {
1081
- parsedToken = Number.parseInt(usedToken);
1082
- }
1083
- return accTokens + parsedToken;
1084
- }, 0);
1098
+ const usedTokens = firstResponse.reduce(
1099
+ (accTokens, usedToken, index) => {
1100
+ let parsedToken = 0;
1101
+ if (index % 2) {
1102
+ parsedToken = Number.parseInt(usedToken);
1103
+ }
1104
+ return accTokens + parsedToken;
1105
+ },
1106
+ 0
1107
+ );
1085
1108
  return {
1086
1109
  remaining: Math.max(0, tokens - usedTokens),
1087
1110
  reset: (bucket + 1) * windowDuration
@@ -1092,14 +1115,11 @@ var MultiRegionRatelimit = class extends Ratelimit {
1092
1115
  if (ctx.cache) {
1093
1116
  ctx.cache.pop(identifier);
1094
1117
  }
1095
- await Promise.all(ctx.regionContexts.map((regionContext) => {
1096
- safeEval(
1097
- regionContext,
1098
- RESET_SCRIPT,
1099
- [pattern],
1100
- [null]
1101
- );
1102
- }));
1118
+ await Promise.all(
1119
+ ctx.regionContexts.map((regionContext) => {
1120
+ safeEval(regionContext, RESET_SCRIPT, [pattern], [null]);
1121
+ })
1122
+ );
1103
1123
  }
1104
1124
  });
1105
1125
  }
@@ -1124,7 +1144,14 @@ var MultiRegionRatelimit = class extends Ratelimit {
1124
1144
  const windowDuration = ms(window);
1125
1145
  return () => ({
1126
1146
  async limit(ctx, identifier, rate) {
1127
- if (ctx.cache) {
1147
+ const requestId = randomId();
1148
+ const now = Date.now();
1149
+ const currentWindow = Math.floor(now / windowSize);
1150
+ const currentKey = [identifier, currentWindow].join(":");
1151
+ const previousWindow = currentWindow - 1;
1152
+ const previousKey = [identifier, previousWindow].join(":");
1153
+ const incrementBy = rate ?? 1;
1154
+ if (ctx.cache && incrementBy > 0) {
1128
1155
  const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
1129
1156
  if (blocked) {
1130
1157
  return {
@@ -1137,13 +1164,6 @@ var MultiRegionRatelimit = class extends Ratelimit {
1137
1164
  };
1138
1165
  }
1139
1166
  }
1140
- const requestId = randomId();
1141
- const now = Date.now();
1142
- const currentWindow = Math.floor(now / windowSize);
1143
- const currentKey = [identifier, currentWindow].join(":");
1144
- const previousWindow = currentWindow - 1;
1145
- const previousKey = [identifier, previousWindow].join(":");
1146
- const incrementBy = rate ? Math.max(1, rate) : 1;
1147
1167
  const dbs = ctx.regionContexts.map((regionContext) => ({
1148
1168
  redis: regionContext.redis,
1149
1169
  request: safeEval(
@@ -1155,37 +1175,49 @@ var MultiRegionRatelimit = class extends Ratelimit {
1155
1175
  )
1156
1176
  }));
1157
1177
  const percentageInCurrent = now % windowDuration / windowDuration;
1158
- const [current, previous, success] = await Promise.any(dbs.map((s) => s.request));
1178
+ const [current, previous, success] = await Promise.any(
1179
+ dbs.map((s) => s.request)
1180
+ );
1159
1181
  if (success) {
1160
1182
  current.push(requestId, incrementBy.toString());
1161
1183
  }
1162
- const previousUsedTokens = previous.reduce((accTokens, usedToken, index) => {
1163
- let parsedToken = 0;
1164
- if (index % 2) {
1165
- parsedToken = Number.parseInt(usedToken);
1166
- }
1167
- return accTokens + parsedToken;
1168
- }, 0);
1169
- const currentUsedTokens = current.reduce((accTokens, usedToken, index) => {
1170
- let parsedToken = 0;
1171
- if (index % 2) {
1172
- parsedToken = Number.parseInt(usedToken);
1173
- }
1174
- return accTokens + parsedToken;
1175
- }, 0);
1176
- const previousPartialUsed = Math.ceil(previousUsedTokens * (1 - percentageInCurrent));
1184
+ const previousUsedTokens = previous.reduce(
1185
+ (accTokens, usedToken, index) => {
1186
+ let parsedToken = 0;
1187
+ if (index % 2) {
1188
+ parsedToken = Number.parseInt(usedToken);
1189
+ }
1190
+ return accTokens + parsedToken;
1191
+ },
1192
+ 0
1193
+ );
1194
+ const currentUsedTokens = current.reduce(
1195
+ (accTokens, usedToken, index) => {
1196
+ let parsedToken = 0;
1197
+ if (index % 2) {
1198
+ parsedToken = Number.parseInt(usedToken);
1199
+ }
1200
+ return accTokens + parsedToken;
1201
+ },
1202
+ 0
1203
+ );
1204
+ const previousPartialUsed = Math.ceil(
1205
+ previousUsedTokens * (1 - percentageInCurrent)
1206
+ );
1177
1207
  const usedTokens = previousPartialUsed + currentUsedTokens;
1178
1208
  const remaining = tokens - usedTokens;
1179
1209
  async function sync() {
1180
1210
  const res = await Promise.all(dbs.map((s) => s.request));
1181
- const allCurrentIds = [...new Set(
1182
- res.flatMap(([current2]) => current2).reduce((acc, curr, index) => {
1183
- if (index % 2 === 0) {
1184
- acc.push(curr);
1185
- }
1186
- return acc;
1187
- }, [])
1188
- ).values()];
1211
+ const allCurrentIds = [
1212
+ ...new Set(
1213
+ res.flatMap(([current2]) => current2).reduce((acc, curr, index) => {
1214
+ if (index % 2 === 0) {
1215
+ acc.push(curr);
1216
+ }
1217
+ return acc;
1218
+ }, [])
1219
+ ).values()
1220
+ ];
1189
1221
  for (const db of dbs) {
1190
1222
  const [current2, _previous, _success] = await db.request;
1191
1223
  const dbIds = current2.reduce((ids, currentId, index) => {
@@ -1194,13 +1226,16 @@ var MultiRegionRatelimit = class extends Ratelimit {
1194
1226
  }
1195
1227
  return ids;
1196
1228
  }, []);
1197
- const usedDbTokens = current2.reduce((accTokens, usedToken, index) => {
1198
- let parsedToken = 0;
1199
- if (index % 2) {
1200
- parsedToken = Number.parseInt(usedToken);
1201
- }
1202
- return accTokens + parsedToken;
1203
- }, 0);
1229
+ const usedDbTokens = current2.reduce(
1230
+ (accTokens, usedToken, index) => {
1231
+ let parsedToken = 0;
1232
+ if (index % 2) {
1233
+ parsedToken = Number.parseInt(usedToken);
1234
+ }
1235
+ return accTokens + parsedToken;
1236
+ },
1237
+ 0
1238
+ );
1204
1239
  if (usedDbTokens >= tokens) {
1205
1240
  continue;
1206
1241
  }
@@ -1214,8 +1249,12 @@ var MultiRegionRatelimit = class extends Ratelimit {
1214
1249
  }
1215
1250
  }
1216
1251
  const reset = (currentWindow + 1) * windowDuration;
1217
- if (ctx.cache && !success) {
1218
- ctx.cache.blockUntil(identifier, reset);
1252
+ if (ctx.cache) {
1253
+ if (!success) {
1254
+ ctx.cache.blockUntil(identifier, reset);
1255
+ } else if (incrementBy < 0) {
1256
+ ctx.cache.pop(identifier);
1257
+ }
1219
1258
  }
1220
1259
  return {
1221
1260
  success: Boolean(success),
@@ -1252,14 +1291,11 @@ var MultiRegionRatelimit = class extends Ratelimit {
1252
1291
  if (ctx.cache) {
1253
1292
  ctx.cache.pop(identifier);
1254
1293
  }
1255
- await Promise.all(ctx.regionContexts.map((regionContext) => {
1256
- safeEval(
1257
- regionContext,
1258
- RESET_SCRIPT,
1259
- [pattern],
1260
- [null]
1261
- );
1262
- }));
1294
+ await Promise.all(
1295
+ ctx.regionContexts.map((regionContext) => {
1296
+ safeEval(regionContext, RESET_SCRIPT, [pattern], [null]);
1297
+ })
1298
+ );
1263
1299
  }
1264
1300
  });
1265
1301
  }
@@ -1308,7 +1344,8 @@ var RegionRatelimit = class extends Ratelimit {
1308
1344
  async limit(ctx, identifier, rate) {
1309
1345
  const bucket = Math.floor(Date.now() / windowDuration);
1310
1346
  const key = [identifier, bucket].join(":");
1311
- if (ctx.cache) {
1347
+ const incrementBy = rate ?? 1;
1348
+ if (ctx.cache && incrementBy > 0) {
1312
1349
  const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
1313
1350
  if (blocked) {
1314
1351
  return {
@@ -1321,7 +1358,6 @@ var RegionRatelimit = class extends Ratelimit {
1321
1358
  };
1322
1359
  }
1323
1360
  }
1324
- const incrementBy = rate ? Math.max(1, rate) : 1;
1325
1361
  const usedTokensAfterUpdate = await safeEval(
1326
1362
  ctx,
1327
1363
  SCRIPTS.singleRegion.fixedWindow.limit,
@@ -1331,8 +1367,12 @@ var RegionRatelimit = class extends Ratelimit {
1331
1367
  const success = usedTokensAfterUpdate <= tokens;
1332
1368
  const remainingTokens = Math.max(0, tokens - usedTokensAfterUpdate);
1333
1369
  const reset = (bucket + 1) * windowDuration;
1334
- if (ctx.cache && !success) {
1335
- ctx.cache.blockUntil(identifier, reset);
1370
+ if (ctx.cache) {
1371
+ if (!success) {
1372
+ ctx.cache.blockUntil(identifier, reset);
1373
+ } else if (incrementBy < 0) {
1374
+ ctx.cache.pop(identifier);
1375
+ }
1336
1376
  }
1337
1377
  return {
1338
1378
  success,
@@ -1395,7 +1435,8 @@ var RegionRatelimit = class extends Ratelimit {
1395
1435
  const currentKey = [identifier, currentWindow].join(":");
1396
1436
  const previousWindow = currentWindow - 1;
1397
1437
  const previousKey = [identifier, previousWindow].join(":");
1398
- if (ctx.cache) {
1438
+ const incrementBy = rate ?? 1;
1439
+ if (ctx.cache && incrementBy > 0) {
1399
1440
  const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
1400
1441
  if (blocked) {
1401
1442
  return {
@@ -1408,7 +1449,6 @@ var RegionRatelimit = class extends Ratelimit {
1408
1449
  };
1409
1450
  }
1410
1451
  }
1411
- const incrementBy = rate ? Math.max(1, rate) : 1;
1412
1452
  const remainingTokens = await safeEval(
1413
1453
  ctx,
1414
1454
  SCRIPTS.singleRegion.slidingWindow.limit,
@@ -1417,8 +1457,12 @@ var RegionRatelimit = class extends Ratelimit {
1417
1457
  );
1418
1458
  const success = remainingTokens >= 0;
1419
1459
  const reset = (currentWindow + 1) * windowSize;
1420
- if (ctx.cache && !success) {
1421
- ctx.cache.blockUntil(identifier, reset);
1460
+ if (ctx.cache) {
1461
+ if (!success) {
1462
+ ctx.cache.blockUntil(identifier, reset);
1463
+ } else if (incrementBy < 0) {
1464
+ ctx.cache.pop(identifier);
1465
+ }
1422
1466
  }
1423
1467
  return {
1424
1468
  success,
@@ -1476,7 +1520,9 @@ var RegionRatelimit = class extends Ratelimit {
1476
1520
  const intervalDuration = ms(interval);
1477
1521
  return () => ({
1478
1522
  async limit(ctx, identifier, rate) {
1479
- if (ctx.cache) {
1523
+ const now = Date.now();
1524
+ const incrementBy = rate ?? 1;
1525
+ if (ctx.cache && incrementBy > 0) {
1480
1526
  const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
1481
1527
  if (blocked) {
1482
1528
  return {
@@ -1489,8 +1535,6 @@ var RegionRatelimit = class extends Ratelimit {
1489
1535
  };
1490
1536
  }
1491
1537
  }
1492
- const now = Date.now();
1493
- const incrementBy = rate ? Math.max(1, rate) : 1;
1494
1538
  const [remaining, reset] = await safeEval(
1495
1539
  ctx,
1496
1540
  SCRIPTS.singleRegion.tokenBucket.limit,
@@ -1498,8 +1542,12 @@ var RegionRatelimit = class extends Ratelimit {
1498
1542
  [maxTokens, intervalDuration, refillRate, now, incrementBy]
1499
1543
  );
1500
1544
  const success = remaining >= 0;
1501
- if (ctx.cache && !success) {
1502
- ctx.cache.blockUntil(identifier, reset);
1545
+ if (ctx.cache) {
1546
+ if (!success) {
1547
+ ctx.cache.blockUntil(identifier, reset);
1548
+ } else if (incrementBy < 0) {
1549
+ ctx.cache.pop(identifier);
1550
+ }
1503
1551
  }
1504
1552
  return {
1505
1553
  success,
@@ -1571,10 +1619,10 @@ var RegionRatelimit = class extends Ratelimit {
1571
1619
  const bucket = Math.floor(Date.now() / windowDuration);
1572
1620
  const key = [identifier, bucket].join(":");
1573
1621
  const reset = (bucket + 1) * windowDuration;
1574
- const incrementBy = rate ? Math.max(1, rate) : 1;
1622
+ const incrementBy = rate ?? 1;
1575
1623
  const hit = typeof ctx.cache.get(key) === "number";
1576
1624
  if (hit) {
1577
- const cachedTokensAfterUpdate = ctx.cache.incr(key);
1625
+ const cachedTokensAfterUpdate = ctx.cache.incr(key, incrementBy);
1578
1626
  const success = cachedTokensAfterUpdate < tokens;
1579
1627
  const pending = success ? safeEval(
1580
1628
  ctx,