Skip to content

Commit

Permalink
feat: use cloudflare's ratelimiter if possible (#2544)
Browse files Browse the repository at this point in the history
* feat: use cloudflare's ratelimiter if possible

According to our data, these 5 ratelimit configs cover nearly 75% of our
ratelimit operations.

By using cloudflare for these, I expect our latency to drop and accuracy
to increase

* fix: only use cf in async mode
  • Loading branch information
chronark authored Oct 23, 2024
1 parent 8762d13 commit 10d978b
Show file tree
Hide file tree
Showing 5 changed files with 751 additions and 830 deletions.
9 changes: 9 additions & 0 deletions apps/api/src/pkg/env.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { z } from "zod";
import type { MessageBody } from "./key_migration/message";

export const cloudflareRatelimiter = z.custom<{
limit: (opts: { key: string }) => Promise<{ success: boolean }>;
}>((r) => typeof r.limit === "function");

export const zEnv = z.object({
VERSION: z.string().default("unknown"),
DATABASE_HOST: z.string(),
Expand Down Expand Up @@ -42,6 +46,11 @@ export const zEnv = z.object({
return 0;
}
}),
RL_10_60s: cloudflareRatelimiter,
RL_30_60s: cloudflareRatelimiter,
RL_200_60s: cloudflareRatelimiter,
RL_500_10s: cloudflareRatelimiter,
RL_200_10s: cloudflareRatelimiter,
});

export type Env = z.infer<typeof zEnv>;
32 changes: 29 additions & 3 deletions apps/api/src/pkg/ratelimit/client.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Err, Ok, type Result } from "@unkey/error";
import type { Logger } from "@unkey/worker-logging";
import type { Metrics } from "../metrics";

import { cloudflareRatelimiter } from "../env";
import type { Context } from "../hono/app";
import type { Metrics } from "../metrics";
import { retry } from "../util/retry";
import { Agent } from "./agent";
import {
Expand All @@ -11,7 +11,6 @@ import {
type RatelimitRequest,
type RatelimitResponse,
} from "./interface";

export class AgentRatelimiter implements RateLimiter {
private readonly logger: Logger;
private readonly metrics: Metrics;
Expand Down Expand Up @@ -67,7 +66,34 @@ export class AgentRatelimiter implements RateLimiter {
req: RatelimitRequest,
): Promise<Result<RatelimitResponse, RatelimitError>> {
const start = performance.now();
try {
if (req.async) {
// Construct a binding key that could match a configured ratelimiter
const lookup = `RL_${req.limit}_${Math.round(req.interval / 1000)}s` as keyof typeof c.env;
const binding = c.env[lookup];

if (binding) {
const res = await cloudflareRatelimiter.parse(binding).limit({ key: req.identifier });

this.metrics.emit({
metric: "metric.ratelimit",
workspaceId: req.workspaceId,
namespaceId: req.namespaceId,
latency: performance.now() - start,
identifier: req.identifier,
mode: "async",
error: false,
success: res.success,
source: "cloudflare",
});
return Ok({ pass: res.success, reset: -1, current: -1, remaining: -1, triggered: null });
}
}
} catch (err) {
this.logger.error("cfrl failed, falling back to agent", {
error: (err as Error).message,
});
}
const res = await this._limit(c, req);
this.metrics.emit({
metric: "metric.ratelimit",
Expand Down
155 changes: 155 additions & 0 deletions apps/api/wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,44 @@ bindings = [
{ name = "DO_USAGELIMIT", class_name = "DurableObjectUsagelimiter" },
]

[[unsafe.bindings]]
# The nameing scheme is important, because we're dynamically constructing
# these in the api code
#
# RL_{LIMIT}_{DURATION}s
#
# The namespace_id schema is somewhat made up though.
# I prefixed everything with 9900 -> 9900{limit}{duration}
name = "RL_10_60s"
type = "ratelimit"
namespace_id = "99001060"
simple = { limit = 10, period = 60}

[[unsafe.bindings]]
name = "RL_30_60s"
type = "ratelimit"
namespace_id = "99003060"
simple = { limit = 30, period = 60}

[[unsafe.bindings]]
name = "RL_200_60s"
type = "ratelimit"
namespace_id = "990020060"
simple = { limit = 200, period = 60}

[[unsafe.bindings]]
name = "RL_500_10s"
type = "ratelimit"
namespace_id = "990050010"
simple = { limit = 500, period = 10}

[[unsafe.bindings]]
name = "RL_200_10s"
type = "ratelimit"
namespace_id = "990020010"
simple = { limit = 200, period = 10}


[queues]
consumers = [
{ queue = "key-migrations-development", max_batch_size = 10, max_retries = 10, dead_letter_queue = "key-migrations-development-dlq" },
Expand Down Expand Up @@ -62,6 +100,45 @@ consumers = [
{ queue = "key-migrations-preview-dlq", max_batch_size = 10, max_retries = 10 },
]

[[env.preview.unsafe.bindings]]
# The nameing scheme is important, because we're dynamically constructing
# these in the api code
#
# RL_{LIMIT}_{DURATION}s
#
# The namespace_id schema is somewhat made up though.
# I prefixed everything with 9900 -> 9900{limit}{duration}
name = "RL_10_60s"
type = "ratelimit"
namespace_id = "99001060"
simple = { limit = 10, period = 60}

[[env.preview.unsafe.bindings]]
name = "RL_30_60s"
type = "ratelimit"
namespace_id = "99003060"
simple = { limit = 30, period = 60}

[[env.preview.unsafe.bindings]]
name = "RL_200_60s"
type = "ratelimit"
namespace_id = "990020060"
simple = { limit = 200, period = 60}

[[env.preview.unsafe.bindings]]
name = "RL_500_10s"
type = "ratelimit"
namespace_id = "990050010"
simple = { limit = 500, period = 10}

[[env.preview.unsafe.bindings]]
name = "RL_200_10s"
type = "ratelimit"
namespace_id = "990020010"
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.
# all settings must be the same as production, except for the route pattern
[env.canary]
Expand All @@ -81,6 +158,45 @@ consumers = [
{ queue = "key-migrations-canary", max_batch_size = 10, max_retries = 10, dead_letter_queue = "key-migrations-canary-dlq" },
{ queue = "key-migrations-canary-dlq", max_batch_size = 10, max_retries = 10 },
]

[[env.canary.unsafe.bindings]]
# The nameing scheme is important, because we're dynamically constructing
# these in the api code
#
# RL_{LIMIT}_{DURATION}s
#
# The namespace_id schema is somewhat made up though.
# I prefixed everything with 9900 -> 9900{limit}{duration}
name = "RL_10_60s"
type = "ratelimit"
namespace_id = "99001060"
simple = { limit = 10, period = 60}

[[env.canary.unsafe.bindings]]
name = "RL_30_60s"
type = "ratelimit"
namespace_id = "99003060"
simple = { limit = 30, period = 60}

[[env.canary.unsafe.bindings]]
name = "RL_200_60s"
type = "ratelimit"
namespace_id = "990020060"
simple = { limit = 200, period = 60}

[[env.canary.unsafe.bindings]]
name = "RL_500_10s"
type = "ratelimit"
namespace_id = "990050010"
simple = { limit = 500, period = 10}

[[env.canary.unsafe.bindings]]
name = "RL_200_10s"
type = "ratelimit"
namespace_id = "990020010"
simple = { limit = 200, period = 10}


[env.production]
vars = { ENVIRONMENT = "production", SYNC_RATELIMIT_ON_NO_DATA = "1" }
route = { pattern = "api.unkey.dev", custom_domain = true }
Expand All @@ -104,3 +220,42 @@ consumers = [

[env.production.observability]
enabled = true

[[env.production.unsafe.bindings]]
# The nameing scheme is important, because we're dynamically constructing
# these in the api code
#
# RL_{LIMIT}_{DURATION}s
#
# The namespace_id schema is somewhat made up though.
# I prefixed everything with 9900 -> 9900{limit}{duration}
name = "RL_10_60s"
type = "ratelimit"
namespace_id = "99001060"
simple = { limit = 10, period = 60}

[[env.production.unsafe.bindings]]
name = "RL_30_60s"
type = "ratelimit"
namespace_id = "99003060"
simple = { limit = 30, period = 60}

[[env.production.unsafe.bindings]]
name = "RL_200_60s"
type = "ratelimit"
namespace_id = "990020060"
simple = { limit = 200, period = 60}

[[env.production.unsafe.bindings]]
name = "RL_500_10s"
type = "ratelimit"
namespace_id = "990050010"
simple = { limit = 500, period = 10}

[[env.production.unsafe.bindings]]
name = "RL_200_10s"
type = "ratelimit"
namespace_id = "990020010"
simple = { limit = 200, period = 10}


2 changes: 1 addition & 1 deletion internal/metrics/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export const metricSchema = z.discriminatedUnion("metric", [
mode: z.enum(["sync", "async"]),
success: z.boolean().optional(),
error: z.boolean().optional(),
source: z.enum(["agent", "durable_object"]),
source: z.enum(["agent", "durable_object", "cloudflare"]),
}),
z.object({
metric: z.literal("metric.usagelimit"),
Expand Down
Loading

0 comments on commit 10d978b

Please sign in to comment.