diff --git a/examples/cloudflare-rate-limit/index.ts b/examples/cloudflare-rate-limit/index.ts
new file mode 100644
index 0000000000..f3a815d211
--- /dev/null
+++ b/examples/cloudflare-rate-limit/index.ts
@@ -0,0 +1,14 @@
+import { Resource } from "sst/resource";
+
+export default {
+ async fetch(req: Request) {
+ const url = new URL(req.url);
+
+ const outcome = await Resource.MyRateLimit.limit({ key: url.pathname });
+ if (!outcome.success) {
+ return new Response(`Rate limit exceeded for ${url.pathname}`, { status: 429 });
+ }
+
+ return new Response("OK", { status: 200 });
+ },
+};
diff --git a/examples/cloudflare-rate-limit/package.json b/examples/cloudflare-rate-limit/package.json
new file mode 100644
index 0000000000..9c3fc082c8
--- /dev/null
+++ b/examples/cloudflare-rate-limit/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "cloudflare-rate-limit",
+ "version": "1.0.0",
+ "type": "module",
+ "dependencies": {
+ "@cloudflare/workers-types": "^4.20260426.1",
+ "sst": "file:../../sdk/js"
+ }
+}
diff --git a/examples/cloudflare-rate-limit/sst.config.ts b/examples/cloudflare-rate-limit/sst.config.ts
new file mode 100644
index 0000000000..1dfa0a61c2
--- /dev/null
+++ b/examples/cloudflare-rate-limit/sst.config.ts
@@ -0,0 +1,33 @@
+///
+
+/**
+ * ## Cloudflare Rate Limit
+ *
+ * This example creates a Cloudflare Rate Limit and a Worker that applies it.
+ */
+export default $config({
+ app(input) {
+ return {
+ name: "cloudflare-rate-limit",
+ removal: input?.stage === "production" ? "retain" : "remove",
+ home: "cloudflare",
+ };
+ },
+ async run() {
+ const rateLimit = new sst.cloudflare.RateLimit("MyRateLimit", {
+ namespaceId: 1001,
+ limit: 100,
+ period: "1 minute",
+ });
+
+ const worker = new sst.cloudflare.Worker("MyWorker", {
+ handler: "./index.ts",
+ url: true,
+ link: [rateLimit],
+ });
+
+ return {
+ api: worker.url,
+ };
+ },
+});
diff --git a/examples/cloudflare-rate-limit/tsconfig.json b/examples/cloudflare-rate-limit/tsconfig.json
new file mode 100644
index 0000000000..4d3c77fe88
--- /dev/null
+++ b/examples/cloudflare-rate-limit/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "compilerOptions": {
+ "lib": ["esnext"],
+ "types": ["@cloudflare/workers-types"]
+ }
+}
diff --git a/pkg/types/typescript/typescript.go b/pkg/types/typescript/typescript.go
index 345493ba30..d49c70b5b0 100644
--- a/pkg/types/typescript/typescript.go
+++ b/pkg/types/typescript/typescript.go
@@ -27,6 +27,7 @@ var mapping = map[string]string{
"hyperdriveBindings": "Hyperdrive",
"versionMetadataBindings": "WorkerVersionMetadata",
"workflowBindings": "Workflow",
+ "rateLimitBindings": "RateLimit",
}
var header = strings.Join([]string{
diff --git a/platform/src/components/cloudflare/binding.ts b/platform/src/components/cloudflare/binding.ts
index cc901601f4..e5004fe3a8 100644
--- a/platform/src/components/cloudflare/binding.ts
+++ b/platform/src/components/cloudflare/binding.ts
@@ -88,6 +88,17 @@ export interface WorkflowBinding {
};
}
+export interface RateLimitBinding {
+ type: "rateLimitBindings";
+ properties: {
+ namespaceId: Input;
+ simple: Input<{
+ limit: Input;
+ period: Input;
+ }>;
+ };
+}
+
export type Binding =
| AiBinding
| KvBinding
@@ -99,7 +110,8 @@ export type Binding =
| D1DatabaseBinding
| HyperdriveBinding
| VersionMetadataBinding
- | WorkflowBinding;
+ | WorkflowBinding
+ | RateLimitBinding;
export function binding(input: Binding & {}) {
return {
diff --git a/platform/src/components/cloudflare/helpers/wrangler.ts b/platform/src/components/cloudflare/helpers/wrangler.ts
index cee8d207a6..adcb6b0191 100644
--- a/platform/src/components/cloudflare/helpers/wrangler.ts
+++ b/platform/src/components/cloudflare/helpers/wrangler.ts
@@ -53,6 +53,7 @@ export function createWranglerConfig(input: {
const services: Record[] = [];
const queueProducers: Record[] = [];
const workflows: Record[] = [];
+ const rateLimits: Record[] = [];
let ai: Record | undefined;
let versionMetadata: Record | undefined;
@@ -135,6 +136,13 @@ export function createWranglerConfig(input: {
remote: true,
});
break;
+ case "rateLimitBindings":
+ rateLimits.push({
+ name: link.name,
+ namespace_id: stringValue(properties.namespaceId),
+ simple: properties.simple,
+ });
+ break;
}
}
@@ -170,6 +178,9 @@ export function createWranglerConfig(input: {
if (workflows.length > 0) {
config.workflows = workflows;
}
+ if (rateLimits.length > 0) {
+ config.rate_limits = rateLimits;
+ }
return config;
}
diff --git a/platform/src/components/cloudflare/index.ts b/platform/src/components/cloudflare/index.ts
index 0d571c5893..0938d23c84 100644
--- a/platform/src/components/cloudflare/index.ts
+++ b/platform/src/components/cloudflare/index.ts
@@ -15,6 +15,7 @@ export * from "./astro";
export * from "./react-router";
export * from "./tan-stack-start";
export * from "./workflow";
+export * from "./rate-limit";
export { binding } from "./binding.js";
/**
diff --git a/platform/src/components/cloudflare/rate-limit.ts b/platform/src/components/cloudflare/rate-limit.ts
new file mode 100644
index 0000000000..2af65e2aac
--- /dev/null
+++ b/platform/src/components/cloudflare/rate-limit.ts
@@ -0,0 +1,178 @@
+import {
+ ComponentResourceOptions,
+ output,
+ type Input,
+ type Output,
+} from "@pulumi/pulumi";
+import { Component } from "../component";
+import { Link } from "../link";
+import { binding } from "./binding";
+import { toSeconds } from "../duration";
+import { VisibleError } from "../error";
+
+export interface RateLimitArgs {
+ /**
+ * A positive integer that uniquely defines this rate limiting namespace within your Cloudflare account.
+ */
+ namespaceId: Input;
+ /**
+ * The number of allowed requests within the specified period of time.
+ */
+ limit: Input;
+ /**
+ * The duration of the rate limit window. Must be either 10 seconds or 1 minute.
+ */
+ period: Input<"10 seconds" | "1 minute">;
+}
+
+/**
+ * The `RateLimit` component lets you add a [Cloudflare Rate Limit](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/) binding to
+ * your app.
+ *
+ * @example
+ *
+ * #### Minimal example
+ *
+ * ```ts title="sst.config.ts"
+ * const rateLimit = new sst.cloudflare.RateLimit("MyRateLimit", {
+ * namespaceId: 1001,
+ * limit: 100,
+ * period: "1 minute",
+ * });
+ * ```
+ *
+ * #### Link to a worker
+ *
+ * You can link RateLimit to a worker.
+ *
+ * ```ts {4} title="sst.config.ts"
+ * new sst.cloudflare.Worker("MyWorker", {
+ * handler: "./index.ts",
+ * url: true
+ * link: [rateLimit],
+ * });
+ * ```
+ *
+ * Once linked, you can use the SDK to interact with the RateLimit binding.
+ *
+ * ```ts title="index.ts" {7}
+ * import { Resource } from "sst/resource";
+ *
+ * export default {
+ * async fetch(req, env): Promise {
+ * const url = new URL(req.url);
+ *
+ * const outcome = await Resource.MyRateLimit.limit({ key: url.pathname });
+ * if (!outcome.success) {
+ * return new Response(`Rate limit exceeded for ${url.pathname}`, { status: 429 });
+ * }
+ *
+ * return new Response("OK", { status: 200 });
+ * }
+ * }
+ * ```
+ */
+export class RateLimit extends Component implements Link.Linkable {
+ private _namespaceId: Output;
+ private _limit: Output;
+ private _period: Output;
+
+ constructor(
+ name: string,
+ args: RateLimitArgs,
+ opts?: ComponentResourceOptions,
+ ) {
+ super(__pulumiType, name, args, opts);
+
+ const namespaceId = normalizeNamespaceId();
+ const limit = output(args.limit);
+ const period = normalizePeriod();
+
+ this._namespaceId = namespaceId;
+ this._limit = limit;
+ this._period = period;
+
+ function normalizeNamespaceId() {
+ return output(args.namespaceId).apply((namespaceId) => {
+ if (!Number.isInteger(namespaceId) || namespaceId <= 0) {
+ throw new VisibleError(
+ "The `namespaceId` property must be a positive integer.",
+ );
+ }
+
+ return namespaceId.toString();
+ });
+ }
+
+ function normalizePeriod() {
+ return output(args.period).apply(toSeconds);
+ }
+ }
+
+ /**
+ * A unique identifier for the rate limit namespace.
+ */
+ public get namespaceId() {
+ return this._namespaceId;
+ }
+
+ /**
+ * The number of allowed requests within the specified period of time.
+ */
+ public get limit() {
+ return this._limit;
+ }
+
+ /**
+ * The duration of the rate limit window, in seconds.
+ */
+ public get period() {
+ return this._period;
+ }
+
+ /**
+ * When you link a RateLimit binding, it will be available to the worker and you can
+ * interact with it using its [API methods](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/).
+ *
+ * @example
+ * ```ts title="index.ts"
+ * import { Resource } from "sst/resource";
+ *
+ * export default {
+ * async fetch(req, env): Promise {
+ * const url = new URL(req.url);
+ *
+ * const outcome = await Resource.MyRateLimit.limit({ key: url.pathname });
+ * if (!outcome.success) {
+ * return new Response(`Rate limit exceeded for ${url.pathname}`, { status: 429 });
+ * }
+ *
+ * return new Response("OK", { status: 200 });
+ * }
+ * }
+ * ```
+ *
+ * @internal
+ */
+ getSSTLink() {
+ return {
+ properties: {},
+ include: [
+ binding({
+ type: "rateLimitBindings",
+ properties: {
+ namespaceId: this._namespaceId,
+ simple: {
+ limit: this._limit,
+ period: this._period,
+ },
+ },
+ }),
+ ],
+ };
+ }
+}
+
+const __pulumiType = "sst:cloudflare:RateLimit";
+// @ts-expect-error
+RateLimit.__pulumiType = __pulumiType;
diff --git a/platform/src/components/cloudflare/worker.ts b/platform/src/components/cloudflare/worker.ts
index 595b23642e..1ab8028696 100644
--- a/platform/src/components/cloudflare/worker.ts
+++ b/platform/src/components/cloudflare/worker.ts
@@ -528,6 +528,7 @@ export class Worker extends Component implements Link.Linkable {
hyperdriveBindings: "hyperdrive",
versionMetadataBindings: "version_metadata",
workflowBindings: "workflow",
+ rateLimitBindings: "ratelimit"
}[b.binding],
name,
...b.properties,
diff --git a/www/astro.config.mjs b/www/astro.config.mjs
index f8835346da..e1d2012058 100644
--- a/www/astro.config.mjs
+++ b/www/astro.config.mjs
@@ -232,6 +232,7 @@ const sidebar = [
"docs/component/cloudflare/worker",
"docs/component/cloudflare/bucket",
"docs/component/cloudflare/workflow",
+ "docs/component/cloudflare/rate-limit",
{ label: "StaticSite", slug: "docs/component/cloudflare/static-site-v2" },
"docs/component/cloudflare/hyperdrive",
"docs/component/cloudflare/react-router",
diff --git a/www/generate.ts b/www/generate.ts
index ba9048865e..6d500cddc7 100644
--- a/www/generate.ts
+++ b/www/generate.ts
@@ -2537,6 +2537,7 @@ async function buildComponents() {
"../platform/src/components/cloudflare/react-router.ts",
"../platform/src/components/cloudflare/worker.ts",
"../platform/src/components/cloudflare/workflow.ts",
+ "../platform/src/components/cloudflare/rate-limit.ts",
"../platform/src/components/cloudflare/static-site.ts",
"../platform/src/components/cloudflare/static-site-v2.ts",
"../platform/src/components/cloudflare/tan-stack-start.ts",
diff --git a/www/src/content/docs/docs/cloudflare.mdx b/www/src/content/docs/docs/cloudflare.mdx
index 80aa41fa23..c2580b21c3 100644
--- a/www/src/content/docs/docs/cloudflare.mdx
+++ b/www/src/content/docs/docs/cloudflare.mdx
@@ -130,7 +130,7 @@ For scheduled work, use [`Cron`](/docs/component/cloudflare/cron/) to run a work
### More components
-Browse the component docs for [`Worker`](/docs/component/cloudflare/worker/), [`Astro`](/docs/component/cloudflare/astro/), [`Bucket`](/docs/component/cloudflare/bucket/), [`D1`](/docs/component/cloudflare/d1/), [`Kv`](/docs/component/cloudflare/kv/), [`Queue`](/docs/component/cloudflare/queue/), [`Cron`](/docs/component/cloudflare/cron/), [`Ai`](/docs/component/cloudflare/ai/), and [`Workflow`](/docs/component/cloudflare/workflow/).
+Browse the component docs for [`Worker`](/docs/component/cloudflare/worker/), [`Astro`](/docs/component/cloudflare/astro/), [`Bucket`](/docs/component/cloudflare/bucket/), [`D1`](/docs/component/cloudflare/d1/), [`Kv`](/docs/component/cloudflare/kv/), [`Queue`](/docs/component/cloudflare/queue/), [`Cron`](/docs/component/cloudflare/cron/), [`Ai`](/docs/component/cloudflare/ai/), [`Workflow`](/docs/component/cloudflare/workflow/), and [`RateLimit`](/docs/component/cloudflare/rate-limit/).
If you are using Cloudflare DNS with SST, use [`sst.cloudflare.dns`](/docs/component/cloudflare/dns/) with [custom domains](/docs/custom-domains/).