diff --git a/apps/api/src/pkg/env.ts b/apps/api/src/pkg/env.ts index 6a2dc8a8cf..01ab879cd3 100644 --- a/apps/api/src/pkg/env.ts +++ b/apps/api/src/pkg/env.ts @@ -18,6 +18,7 @@ export const zEnv = z.object({ CLOUDFLARE_API_KEY: z.string().optional(), CLOUDFLARE_ZONE_ID: z.string().optional(), ENVIRONMENT: z.enum(["development", "preview", "canary", "production"]).default("development"), + DO_RATELIMIT: z.custom((ns) => typeof ns === "object"), // pretty loose check but it'll do I think DO_USAGELIMIT: z.custom((ns) => typeof ns === "object"), KEY_MIGRATIONS: z.custom>((q) => typeof q === "object").optional(), EMIT_METRICS_LOGS: z diff --git a/apps/api/src/pkg/middleware/init.ts b/apps/api/src/pkg/middleware/init.ts index 9e5b3df5ab..50042fe269 100644 --- a/apps/api/src/pkg/middleware/init.ts +++ b/apps/api/src/pkg/middleware/init.ts @@ -2,7 +2,7 @@ import { Analytics } from "@/pkg/analytics"; import { createConnection } from "@/pkg/db"; import { KeyService } from "@/pkg/keys/service"; -import { AgentRatelimiter } from "@/pkg/ratelimit"; +import { DurableRateLimiter } from "@/pkg/ratelimit"; import { DurableUsageLimiter, NoopUsageLimiter } from "@/pkg/usagelimit"; import { RBAC } from "@unkey/rbac"; import { ConsoleLogger } from "@unkey/worker-logging"; @@ -100,8 +100,8 @@ export function init(): MiddlewareHandler { clickhouseUrl: c.env.CLICKHOUSE_URL, clickhouseInsertUrl: c.env.CLICKHOUSE_INSERT_URL, }); - const rateLimiter = new AgentRatelimiter({ - agent: { url: c.env.AGENT_URL, token: c.env.AGENT_TOKEN }, + const rateLimiter = new DurableRateLimiter({ + namespace: c.env.DO_RATELIMIT, cache: rlMap, logger, metrics, diff --git a/apps/api/src/pkg/ratelimit/do_client.ts b/apps/api/src/pkg/ratelimit/do_client.ts new file mode 100644 index 0000000000..e040d09b64 --- /dev/null +++ b/apps/api/src/pkg/ratelimit/do_client.ts @@ -0,0 +1,240 @@ +import { Err, Ok, type Result } from "@unkey/error"; +import type { Logger } from "@unkey/worker-logging"; +import type { Context } from "hono"; +import { z } from "zod"; +import type { Metrics } from "../metrics"; +import { + type RateLimiter, + RatelimitError, + type RatelimitRequest, + type RatelimitResponse, +} from "./interface"; + +export class DurableRateLimiter implements RateLimiter { + private readonly namespace: DurableObjectNamespace; + private readonly domain: string; + private readonly logger: Logger; + private readonly metrics: Metrics; + private readonly cache: Map; + constructor(opts: { + namespace: DurableObjectNamespace; + + domain?: string; + logger: Logger; + metrics: Metrics; + cache: Map; + }) { + this.namespace = opts.namespace; + this.domain = opts.domain ?? "unkey.dev"; + this.logger = opts.logger; + this.metrics = opts.metrics; + this.cache = opts.cache; + } + + private getId(req: RatelimitRequest): string { + const now = Date.now(); + const window = Math.floor(now / req.interval); + + return [req.identifier, window, req.shard].join("::"); + } + + private setCacheMax(id: string, i: number): number { + const current = this.cache.get(id) ?? 0; + if (i > current) { + this.cache.set(id, i); + return i; + } + return current; + } + + /** + * Do not use + */ + public async multiLimit( + c: Context, + req: Array, + ): Promise> { + const res = await Promise.all(req.map((r) => this.limit(c, r))); + for (const r of res) { + if (r.err) { + return r; + } + if (!r.val.passed) { + return r; + } + } + if (res.length > 0) { + return Ok(res[0].val!); + } + + return Ok({ + current: -1, + passed: true, + reset: -1, + remaining: -1, + triggered: null, + }); + } + + public async limit( + c: Context, + req: RatelimitRequest, + ): Promise> { + const start = performance.now(); + const res = await this._limit(c, req); + this.metrics.emit({ + metric: "metric.ratelimit", + workspaceId: req.workspaceId, + namespaceId: req.namespaceId, + latency: performance.now() - start, + identifier: req.identifier, + mode: req.async ? "async" : "sync", + error: !!res.err, + success: res?.val?.passed, + source: "durable_object", + }); + return res; + } + + private async _limit( + c: Context, + req: RatelimitRequest, + ): Promise> { + const window = Math.floor(Date.now() / req.interval); + const reset = (window + 1) * req.interval; + const cost = req.cost ?? 1; + const id = this.getId(req); + + /** + * Catching identifiers that exceeded the limit already + * + * This might not happen too often, but in extreme cases the cache should hit and we can skip + * the request to the durable object entirely, which speeds everything up and is cheper for us + */ + let current = this.cache.get(id) ?? 0; + if (current >= req.limit) { + return Ok({ + remaining: req.limit - current, + passed: false, + current, + reset, + triggered: req.name, + }); + } + + const p = this.callDurableObject({ + trigger: req.name, + identifier: req.identifier, + objectName: id, + window, + reset, + cost, + limit: req.limit, + }); + + if (!req.async) { + const res = await p; + if (res.val) { + this.setCacheMax(id, res.val.current); + } + return res; + } + + c.executionCtx.waitUntil( + p.then(async (res) => { + if (res.err) { + console.error(res.err.message); + return; + } + this.setCacheMax(id, res.val.current); + + this.metrics.emit({ + workspaceId: req.workspaceId, + metric: "metric.ratelimit.accuracy", + identifier: req.identifier, + namespaceId: req.namespaceId, + responded: current + cost <= req.limit, + correct: res.val.current + cost <= req.limit, + }); + await this.metrics.flush(); + }), + ); + if (current + cost > req.limit) { + return Ok({ + current, + passed: false, + reset, + remaining: req.limit - current, + triggered: req.name, + }); + } + current += cost; + this.cache.set(id, current); + + return Ok({ + passed: true, + current, + reset, + remaining: req.limit - current, + triggered: null, + }); + } + + private getStub(name: string): DurableObjectStub { + return this.namespace.get(this.namespace.idFromName(name)); + } + + private async callDurableObject(req: { + identifier: string; + objectName: string; + window: number; + reset: number; + cost: number; + limit: number; + trigger: string; + }): Promise> { + try { + const url = `https://${this.domain}/limit`; + const res = await this.getStub(req.objectName) + .fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ reset: req.reset, cost: req.cost, limit: req.limit }), + }) + .catch(async (e) => { + this.logger.warn("calling the ratelimit DO failed, retrying ...", { + identifier: req.identifier, + error: (e as Error).message, + }); + + return this.getStub(req.objectName).fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ reset: req.reset, cost: req.cost, limit: req.limit }), + }); + }); + + const json = await res.json(); + const { current, success } = z + .object({ current: z.number(), success: z.boolean() }) + .parse(json); + + return Ok({ + current, + reset: req.reset, + passed: success, + remaining: req.limit - current, + triggered: success ? null : req.trigger, + }); + } catch (e) { + const err = e as Error; + this.logger.error("ratelimit failed", { + identifier: req.identifier, + error: err.message, + stack: err.stack, + cause: err.cause, + }); + return Err(new RatelimitError({ message: err.message })); + } + } +} diff --git a/apps/api/src/pkg/ratelimit/durable_object.ts b/apps/api/src/pkg/ratelimit/durable_object.ts new file mode 100644 index 0000000000..1bf31d97a2 --- /dev/null +++ b/apps/api/src/pkg/ratelimit/durable_object.ts @@ -0,0 +1,72 @@ +import { zValidator } from "@hono/zod-validator"; +import { Hono } from "hono"; +import { z } from "zod"; + +type Memory = { + current: number; + alarmScheduled?: number; +}; + +export class DurableObjectRatelimiter { + private state: DurableObjectState; + private memory: Memory; + private readonly storageKey = "rl"; + private readonly hono = new Hono(); + constructor(state: DurableObjectState) { + this.state = state; + this.state.blockConcurrencyWhile(async () => { + const m = await this.state.storage.get(this.storageKey); + if (m) { + this.memory = m; + } + }); + this.memory ??= { + current: 0, + }; + + this.hono.post( + "/limit", + zValidator( + "json", + z.object({ + reset: z.number().int(), + cost: z.number().int().default(1), + limit: z.number().int(), + }), + ), + async (c) => { + const { reset, cost, limit } = c.req.valid("json"); + if (!this.memory.alarmScheduled) { + this.memory.alarmScheduled = reset; + await this.state.storage.setAlarm(this.memory.alarmScheduled); + } + if (this.memory.current + cost > limit) { + return c.json({ + success: false, + current: this.memory.current, + }); + } + this.memory.current += cost; + + await this.state.storage.put(this.storageKey, this.memory); + + return c.json({ + success: true, + current: this.memory.current, + }); + }, + ); + } + + // Handle HTTP requests from clients. + async fetch(request: Request) { + return this.hono.fetch(request); + } + + /** + * alarm is called to clean up all state, which will remove the durable object from existence. + */ + public async alarm(): Promise { + await this.state.storage.deleteAll(); + } +} diff --git a/apps/api/src/pkg/ratelimit/index.ts b/apps/api/src/pkg/ratelimit/index.ts index e49f795cef..2f942fc118 100644 --- a/apps/api/src/pkg/ratelimit/index.ts +++ b/apps/api/src/pkg/ratelimit/index.ts @@ -1,3 +1,5 @@ export * from "./client"; export * from "./interface"; export * from "./noop"; +export * from "./durable_object"; +export * from "./do_client"; diff --git a/apps/api/src/routes/v1_keys_verifyKey.ratelimit_accuracy.test.ts b/apps/api/src/routes/v1_keys_verifyKey.ratelimit_accuracy.test.ts index 01a00c833b..4095c93cdc 100644 --- a/apps/api/src/routes/v1_keys_verifyKey.ratelimit_accuracy.test.ts +++ b/apps/api/src/routes/v1_keys_verifyKey.ratelimit_accuracy.test.ts @@ -17,29 +17,60 @@ const testCases: { rps: number; seconds: number; }[] = [ + // Very short window, high throughput + { + limit: 50, + duration: 1000, // 1s window + rps: 200, // 4x the limit + seconds: 15, // 15 windows + }, + + // Short window, burst traffic + { + limit: 100, + duration: 5000, // 5s window + rps: 300, // 15x the limit + seconds: 65, // 13 windows + }, + + // Medium window, steady traffic { limit: 200, - duration: 60_000, - rps: 100, - seconds: 60, + duration: 30000, // 30s window + rps: 50, // 7.5x the limit + seconds: 420, // 14 windows }, + + // Edge case: tiny limit { - limit: 10, - duration: 60000, - rps: 10, - seconds: 60, + limit: 5, + duration: 10000, // 10s window + rps: 20, // 40x the limit + seconds: 140, // 14 windows }, + + // Edge case: high limit, short window { - limit: 500, - duration: 10000, - rps: 20, - seconds: 20, + limit: 1000, + duration: 15000, // 15s window + rps: 200, // 3x the limit + seconds: 180, // 12 windows }, + + // Real world: API limit scenario { limit: 500, - duration: 10000, - rps: 100, - seconds: 30, + duration: 60000, // 60s window + rps: 50, // 6x the limit + seconds: 780, // 13 windows + }, + + // High volume, medium window + { + limit: 1000, + duration: 60000, // 60s window + rps: 100, // 6x the limit + seconds: 720, // 12 windows }, ]; @@ -81,7 +112,7 @@ for (const { limit, duration, rps, seconds } of testCases) { }, 0); const exactLimit = Math.min(results.length, (limit / (duration / 1000)) * seconds); - const upperLimit = Math.round(exactLimit * 2.5); + const upperLimit = Math.round(exactLimit * 1.5); const lowerLimit = Math.round(exactLimit * 0.95); console.info({ name, passed, exactLimit, upperLimit, lowerLimit }); t.expect(passed).toBeGreaterThanOrEqual(lowerLimit); diff --git a/apps/api/src/routes/v1_ratelimits_limit.accuracy.test.ts b/apps/api/src/routes/v1_ratelimits_limit.accuracy.test.ts index f441c1383c..bc58da71e7 100644 --- a/apps/api/src/routes/v1_ratelimits_limit.accuracy.test.ts +++ b/apps/api/src/routes/v1_ratelimits_limit.accuracy.test.ts @@ -17,32 +17,62 @@ const testCases: { rps: number; seconds: number; }[] = [ + // Very short window, high throughput + { + limit: 50, + duration: 1000, // 1s window + rps: 200, // 4x the limit + seconds: 15, // 15 windows + }, + + // Short window, burst traffic + { + limit: 100, + duration: 5000, // 5s window + rps: 300, // 15x the limit + seconds: 65, // 13 windows + }, + + // Medium window, steady traffic { limit: 200, - duration: 60_000, - rps: 100, - seconds: 60, + duration: 30000, // 30s window + rps: 50, // 7.5x the limit + seconds: 420, // 14 windows + }, + + // Edge case: tiny limit + { + limit: 5, + duration: 10000, // 10s window + rps: 20, // 40x the limit + seconds: 140, // 14 windows }, + + // Edge case: high limit, short window { - limit: 10, - duration: 60000, - rps: 10, - seconds: 60, + limit: 1000, + duration: 15000, // 15s window + rps: 200, // 3x the limit + seconds: 180, // 12 windows }, + + // Real world: API limit scenario { limit: 500, - duration: 10000, - rps: 20, - seconds: 20, + duration: 60000, // 60s window + rps: 50, // 6x the limit + seconds: 780, // 13 windows }, + + // High volume, medium window { - limit: 500, - duration: 10000, - rps: 100, - seconds: 30, + limit: 1000, + duration: 60000, // 60s window + rps: 100, // 6x the limit + seconds: 720, // 12 windows }, ]; - for (const { limit, duration, rps, seconds } of testCases) { const name = `[${limit} / ${duration / 1000}s], attacked with ${rps} rps for ${seconds}s`; test(name, { skip: process.env.TEST_LOCAL, retry: 3, timeout: 600_000 }, async (t) => { @@ -84,9 +114,9 @@ for (const { limit, duration, rps, seconds } of testCases) { }, 0); const exactLimit = Math.min(results.length, (limit / (duration / 1000)) * seconds); - const upperLimit = Math.round(exactLimit * 2.5); + const upperLimit = Math.round(exactLimit * 1.5); const lowerLimit = Math.round(exactLimit * 0.95); - console.info({ name, passed, exactLimit, upperLimit, lowerLimit }); + console.info({ requests: results.length, name, passed, exactLimit, upperLimit, lowerLimit }); t.expect(passed).toBeGreaterThanOrEqual(lowerLimit); t.expect(passed).toBeLessThanOrEqual(upperLimit); }); diff --git a/apps/api/src/worker.ts b/apps/api/src/worker.ts index e5c7d6e6de..b9a10a767f 100644 --- a/apps/api/src/worker.ts +++ b/apps/api/src/worker.ts @@ -23,6 +23,8 @@ import { registerLegacyKeysVerifyKey } from "./routes/legacy_keys_verifyKey"; // Export Durable Objects for cloudflare export { DurableObjectUsagelimiter } from "@/pkg/usagelimit/durable_object"; +export { DurableObjectRatelimiter } from "@/pkg/ratelimit/durable_object"; + import { cors, init, metrics } from "@/pkg/middleware"; import type { MessageBatch } from "@cloudflare/workers-types"; import { ConsoleLogger } from "@unkey/worker-logging"; diff --git a/apps/api/wrangler.custom.toml b/apps/api/wrangler.custom.toml index e9dcdf764f..d918bd07db 100644 --- a/apps/api/wrangler.custom.toml +++ b/apps/api/wrangler.custom.toml @@ -14,6 +14,7 @@ route = { pattern = "__CUSTOM_DOMAIN__", custom_domain = true } [durable_objects] bindings = [ + { name = "DO_RATELIMIT", class_name = "DurableObjectRatelimiter" }, { name = "DO_USAGELIMIT", class_name = "DurableObjectUsagelimiter" }, ] @@ -27,6 +28,10 @@ tag = "v2" deleted_classes = ["DurableObjectRatelimiter"] +[[migrations]] +tag = "v3" +new_classes = ["DurableObjectRatelimiter"] + [[unsafe.bindings]] # The nameing scheme is important, because we're dynamically constructing @@ -39,53 +44,50 @@ deleted_classes = ["DurableObjectRatelimiter"] name = "RL_10_60s" type = "ratelimit" namespace_id = "99001060" -simple = { limit = 10, period = 60} +simple = { limit = 10, period = 60 } [[unsafe.bindings]] name = "RL_30_60s" type = "ratelimit" namespace_id = "99003060" -simple = { limit = 30, period = 60} +simple = { limit = 30, period = 60 } [[unsafe.bindings]] name = "RL_50_60s" type = "ratelimit" namespace_id = "99005060" -simple = { limit = 50, period = 60} +simple = { limit = 50, period = 60 } [[unsafe.bindings]] name = "RL_200_60s" type = "ratelimit" namespace_id = "990020060" -simple = { limit = 200, period = 60} +simple = { limit = 200, period = 60 } [[unsafe.bindings]] name = "RL_600_60s" type = "ratelimit" namespace_id = "990060060" -simple = { limit = 600, period = 60} - +simple = { limit = 600, period = 60 } [[unsafe.bindings]] name = "RL_1_10s" type = "ratelimit" namespace_id = "9900110" -simple = { limit = 1, period = 10} +simple = { limit = 1, period = 10 } [[unsafe.bindings]] name = "RL_500_10s" type = "ratelimit" namespace_id = "990050010" -simple = { limit = 500, period = 10} +simple = { limit = 500, period = 10 } [[unsafe.bindings]] name = "RL_200_10s" type = "ratelimit" namespace_id = "990020010" -simple = { limit = 200, period = 10} - - +simple = { limit = 200, period = 10 } diff --git a/apps/api/wrangler.toml b/apps/api/wrangler.toml index bb5e1b2dc5..8741e232c5 100644 --- a/apps/api/wrangler.toml +++ b/apps/api/wrangler.toml @@ -10,6 +10,7 @@ enabled = false ## these are in local dev [durable_objects] bindings = [ + { name = "DO_RATELIMIT", class_name = "DurableObjectRatelimiter" }, { name = "DO_USAGELIMIT", class_name = "DurableObjectUsagelimiter" }, ] @@ -25,6 +26,12 @@ new_classes = ["DurableObjectUsagelimiter"] tag = "v3" deleted_classes = ["DurableObjectRatelimiter"] + +[[migrations]] +tag = "v4" +new_classes = ["DurableObjectRatelimiter"] + + [env.development] route = { pattern = "development-api.unkey.dev", custom_domain = true } vars = { ENVIRONMENT = "development" } @@ -32,6 +39,7 @@ vars = { ENVIRONMENT = "development" } [env.development.durable_objects] bindings = [ + { name = "DO_RATELIMIT", class_name = "DurableObjectRatelimiter" }, { name = "DO_USAGELIMIT", class_name = "DurableObjectUsagelimiter" }, ] @@ -46,54 +54,53 @@ bindings = [ name = "RL_10_60s" type = "ratelimit" namespace_id = "99001060" -simple = { limit = 10, period = 60} +simple = { limit = 10, period = 60 } [[unsafe.bindings]] name = "RL_30_60s" type = "ratelimit" namespace_id = "99003060" -simple = { limit = 30, period = 60} +simple = { limit = 30, period = 60 } [[unsafe.bindings]] name = "RL_50_60s" type = "ratelimit" namespace_id = "99005060" -simple = { limit = 50, period = 60} +simple = { limit = 50, period = 60 } [[unsafe.bindings]] name = "RL_200_60s" type = "ratelimit" namespace_id = "990020060" -simple = { limit = 200, period = 60} +simple = { limit = 200, period = 60 } [[unsafe.bindings]] name = "RL_600_60s" type = "ratelimit" namespace_id = "990060060" -simple = { limit = 600, period = 60} - +simple = { limit = 600, period = 60 } [[unsafe.bindings]] name = "RL_1_10s" type = "ratelimit" namespace_id = "9900110" -simple = { limit = 1, period = 10} +simple = { limit = 1, period = 10 } [[unsafe.bindings]] name = "RL_500_10s" type = "ratelimit" namespace_id = "990050010" -simple = { limit = 500, period = 10} +simple = { limit = 500, period = 10 } [[unsafe.bindings]] name = "RL_200_10s" type = "ratelimit" namespace_id = "990020010" -simple = { limit = 200, period = 10} +simple = { limit = 200, period = 10 } [queues] @@ -112,6 +119,7 @@ route = { pattern = "preview-api.unkey.dev", custom_domain = true } [env.preview.durable_objects] bindings = [ + { name = "DO_RATELIMIT", class_name = "DurableObjectRatelimiter" }, { name = "DO_USAGELIMIT", class_name = "DurableObjectUsagelimiter" }, ] @@ -134,53 +142,52 @@ consumers = [ name = "RL_10_60s" type = "ratelimit" namespace_id = "99001060" -simple = { limit = 10, period = 60} +simple = { limit = 10, period = 60 } [[env.preview.unsafe.bindings]] name = "RL_30_60s" type = "ratelimit" namespace_id = "99003060" -simple = { limit = 30, period = 60} +simple = { limit = 30, period = 60 } [[env.preview.unsafe.bindings]] name = "RL_50_60s" type = "ratelimit" namespace_id = "99005060" -simple = { limit = 50, period = 60} +simple = { limit = 50, period = 60 } [[env.preview.unsafe.bindings]] name = "RL_200_60s" type = "ratelimit" namespace_id = "990020060" -simple = { limit = 200, period = 60} +simple = { limit = 200, period = 60 } [[env.preview.unsafe.bindings]] name = "RL_600_60s" type = "ratelimit" namespace_id = "990060060" -simple = { limit = 600, period = 60} +simple = { limit = 600, period = 60 } [[env.preview.unsafe.bindings]] name = "RL_1_10s" type = "ratelimit" namespace_id = "9900110" -simple = { limit = 1, period = 10} +simple = { limit = 1, period = 10 } [[env.preview.unsafe.bindings]] name = "RL_500_10s" type = "ratelimit" namespace_id = "990050010" -simple = { limit = 500, period = 10} +simple = { limit = 500, period = 10 } [[env.preview.unsafe.bindings]] name = "RL_200_10s" type = "ratelimit" namespace_id = "990020010" -simple = { limit = 200, period = 10} - +simple = { limit = 200, period = 10 } # canary is a special environment that is used to test new code by a small percentage of users before it is rolled out to the rest of the world. @@ -192,6 +199,7 @@ route = { pattern = "canary.unkey.dev", custom_domain = true } [env.canary.durable_objects] bindings = [ + { name = "DO_RATELIMIT", class_name = "DurableObjectRatelimiter" }, { name = "DO_USAGELIMIT", class_name = "DurableObjectUsagelimiter" }, ] @@ -214,52 +222,52 @@ consumers = [ name = "RL_10_60s" type = "ratelimit" namespace_id = "99001060" -simple = { limit = 10, period = 60} +simple = { limit = 10, period = 60 } [[env.canary.unsafe.bindings]] name = "RL_30_60s" type = "ratelimit" namespace_id = "99003060" -simple = { limit = 30, period = 60} +simple = { limit = 30, period = 60 } [[env.canary.unsafe.bindings]] name = "RL_50_60s" type = "ratelimit" namespace_id = "99005060" -simple = { limit = 50, period = 60} +simple = { limit = 50, period = 60 } [[env.canary.unsafe.bindings]] name = "RL_200_60s" type = "ratelimit" namespace_id = "990020060" -simple = { limit = 200, period = 60} +simple = { limit = 200, period = 60 } [[env.canary.unsafe.bindings]] name = "RL_600_60s" type = "ratelimit" namespace_id = "990060060" -simple = { limit = 600, period = 60} +simple = { limit = 600, period = 60 } [[env.canary.unsafe.bindings]] name = "RL_1_10s" type = "ratelimit" namespace_id = "9900110" -simple = { limit = 1, period = 10} +simple = { limit = 1, period = 10 } [[env.canary.unsafe.bindings]] name = "RL_500_10s" type = "ratelimit" namespace_id = "990050010" -simple = { limit = 500, period = 10} +simple = { limit = 500, period = 10 } [[env.canary.unsafe.bindings]] name = "RL_200_10s" type = "ratelimit" namespace_id = "990020010" -simple = { limit = 200, period = 10} +simple = { limit = 200, period = 10 } [env.production] @@ -270,6 +278,7 @@ logpush = true [env.production.durable_objects] bindings = [ + { name = "DO_RATELIMIT", class_name = "DurableObjectRatelimiter" }, { name = "DO_USAGELIMIT", class_name = "DurableObjectUsagelimiter" }, ] @@ -297,49 +306,49 @@ enabled = false name = "RL_10_60s" type = "ratelimit" namespace_id = "99001060" -simple = { limit = 10, period = 60} +simple = { limit = 10, period = 60 } [[env.production.unsafe.bindings]] name = "RL_30_60s" type = "ratelimit" namespace_id = "99003060" -simple = { limit = 30, period = 60} +simple = { limit = 30, period = 60 } [[env.production.unsafe.bindings]] name = "RL_50_60s" type = "ratelimit" namespace_id = "99005060" -simple = { limit = 50, period = 60} +simple = { limit = 50, period = 60 } [[env.production.unsafe.bindings]] name = "RL_200_60s" type = "ratelimit" namespace_id = "990020060" -simple = { limit = 200, period = 60} +simple = { limit = 200, period = 60 } [[env.production.unsafe.bindings]] name = "RL_600_60s" type = "ratelimit" namespace_id = "990060060" -simple = { limit = 600, period = 60} +simple = { limit = 600, period = 60 } [[env.production.unsafe.bindings]] name = "RL_1_10s" type = "ratelimit" namespace_id = "9900110" -simple = { limit = 1, period = 10} +simple = { limit = 1, period = 10 } [[env.production.unsafe.bindings]] name = "RL_500_10s" type = "ratelimit" namespace_id = "990050010" -simple = { limit = 500, period = 10} +simple = { limit = 500, period = 10 } [[env.production.unsafe.bindings]] name = "RL_200_10s" type = "ratelimit" namespace_id = "990020010" -simple = { limit = 200, period = 10} +simple = { limit = 200, period = 10 }