HonoHub Logo

HonoHub

Cloudflare

Rate limiting on Cloudflare Workers with hono-rate-limiter

If you're deploying to Cloudflare Workers, we recommend using Cloudflare's native Rate Limiting API. It's built into the Workers runtime, requires no external dependencies, and provides excellent performance with minimal latency.

Rate Limiting API

Cloudflare's Rate Limiting API is the simplest and most efficient way to add rate limiting to your Cloudflare Workers. It uses Cloudflare's built-in infrastructure at the edge.

Configuration

First, add a rate limit binding to your Wrangler configuration:

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "main": "src/index.ts",
  "ratelimits": [
    {
      "name": "MY_RATE_LIMITER",
      "namespace_id": "1001",
      "simple": {
        "limit": 100,
        "period": 60
      }
    }
  ]
}
main = "src/index.ts"

[[ratelimits]]
name = "MY_RATE_LIMITER"
# An identifier you define, unique to your Cloudflare account (must be an integer)
namespace_id = "1001"
# Limit: number of requests allowed within the period
# Period: duration in seconds (must be 10 or 60)
simple = { limit = 100, period = 60 }

Usage

Use the rateLimiter with the binding option:

import { Hono } from "hono";
import { rateLimiter } from "hono-rate-limiter";

type Env = {
  MY_RATE_LIMITER: RateLimit;
};

const app = new Hono<{ Bindings: Env }>();

app.use(
  rateLimiter<{ Bindings: Env }>({
    binding: (c) => c.env.MY_RATE_LIMITER,
    keyGenerator: (c) => c.req.header("cf-connecting-ip") ?? "",
  })
);

app.get("/", (c) => c.text("Hello!"));

export default app;

How It Works

When you use the binding option, hono-rate-limiter uses Cloudflare's native rate limiting:

  • Rate limits are configured in your Wrangler config, not in code
  • No windowMs or limit options needed (configured in binding)
  • The rate limit check is performed at the edge with minimal latency
  • Rate limits are local to each Cloudflare location

Configuration Options

rateLimiter({
  binding: (c) => c.env.MY_RATE_LIMITER, // Required: Rate limit binding
  keyGenerator: (c) => string,            // Required: Client identifier
  message: "Rate limit exceeded",         // Optional: Response message
  statusCode: 429,                        // Optional: HTTP status
  handler: (c, next, options) => {},      // Optional: Custom handler
  skip: (c) => boolean,                   // Optional: Skip logic
});

Multiple Rate Limits

You can define different rate limits for different types of users:

{
  "ratelimits": [
    {
      "name": "FREE_USER_RATE_LIMITER",
      "namespace_id": "1001",
      "simple": { "limit": 100, "period": 60 }
    },
    {
      "name": "PAID_USER_RATE_LIMITER",
      "namespace_id": "1002",
      "simple": { "limit": 1000, "period": 60 }
    }
  ]
}
# Free user rate limiting
[[ratelimits]]
name = "FREE_USER_RATE_LIMITER"
namespace_id = "1001"
simple = { limit = 100, period = 60 }

# Paid user rate limiting
[[ratelimits]]
name = "PAID_USER_RATE_LIMITER"
namespace_id = "1002"
simple = { limit = 1000, period = 60 }
type Env = {
  FREE_USER_RATE_LIMITER: RateLimit;
  PAID_USER_RATE_LIMITER: RateLimit;
};

app.use(
  rateLimiter<{ Bindings: Env }>({
    binding: (c) => {
      const isPaidUser = c.get("isPaidUser");
      return isPaidUser ? c.env.PAID_USER_RATE_LIMITER : c.env.FREE_USER_RATE_LIMITER;
    },
    keyGenerator: (c) => c.get("userId") ?? "",
  })
);

Complete Example

import { Hono } from "hono";
import { rateLimiter } from "hono-rate-limiter";

type Env = {
  API_RATE_LIMITER: RateLimit;
};

const app = new Hono<{ Bindings: Env }>();

// Apply rate limiting to API routes
app.use(
  "/api/*",
  rateLimiter<{ Bindings: Env }>({
    binding: (c) => c.env.API_RATE_LIMITER,
    keyGenerator: (c) => {
      // Use API key if available, fallback to IP
      return c.req.header("x-api-key") ?? c.req.header("cf-connecting-ip") ?? "";
    },
    message: { error: "Rate limit exceeded", retryAfter: "60s" },
  })
);

app.get("/api/data", (c) => {
  return c.json({ message: "Hello from the API!" });
});

export default app;

With wrangler.toml:

name = "my-rate-limited-api"
main = "src/index.ts"
compatibility_date = "2024-01-01"

[[ratelimits]]
name = "API_RATE_LIMITER"
namespace_id = "1001"
simple = { limit = 100, period = 60 }

Workers KV Store (Legacy)

Legacy

This store is available via the @hono-rate-limiter/cloudflare package. We recommend using the Rate Limiting API instead for new projects.

Use Cloudflare KV for globally distributed rate limiting. KV provides eventual consistency, which is suitable for most rate limiting use cases.

Installation

npm install @hono-rate-limiter/cloudflare

Configuration

Add a KV namespace binding to your wrangler.toml:

[[kv_namespaces]]
binding = "RATE_LIMIT_KV"
id = "your-namespace-id"

Usage

import { Hono } from "hono";
import { rateLimiter } from "hono-rate-limiter";
import { WorkersKVStore } from "@hono-rate-limiter/cloudflare";

type Env = {
  RATE_LIMIT_KV: KVNamespace;
};

const app = new Hono<{ Bindings: Env }>();

app.use(async (c, next) => {
  const limiter = rateLimiter({
    windowMs: 60_000,
    limit: 100,
    keyGenerator: (c) => c.req.header("cf-connecting-ip") ?? "",
    store: new WorkersKVStore({
      namespace: c.env.RATE_LIMIT_KV,
    }),
  });
  return limiter(c, next);
});

export default app;

Configuration Options

new WorkersKVStore({
  namespace: kvNamespace, // Required: KV namespace binding
  prefix: "hrl:",         // Optional: Key prefix (default: "hrl:")
});

KV Expiration Limitation

Cloudflare KV has a minimum expiration of 60 seconds. The store automatically adjusts expiration times to meet this requirement. This doesn't affect rate limiting behavior, which is controlled by resetTime.

Limitations

  • Eventually consistent: May briefly allow over-limit requests
  • 60-second minimum expiration: Cannot use shorter windows

Durable Objects Store (Legacy)

Legacy

This store is available via the @hono-rate-limiter/cloudflare package. We recommend using the Rate Limiting API instead for new projects.

Durable Objects provide strong consistency and are ideal when you need precise rate limiting with transactional guarantees.

Installation

npm install @hono-rate-limiter/cloudflare

Configuration

First, create the Durable Object class by exporting it from your worker:

// Export the Durable Object class
export { DurableObjectRateLimiter } from "@hono-rate-limiter/cloudflare";

Add the Durable Object binding to your wrangler.toml:

[durable_objects]
bindings = [
  { name = "RATE_LIMITER", class_name = "DurableObjectRateLimiter" }
]

[[migrations]]
tag = "v1"
new_classes = ["DurableObjectRateLimiter"]

Usage

import { Hono } from "hono";
import { rateLimiter } from "hono-rate-limiter";
import {
  DurableObjectStore,
  DurableObjectRateLimiter,
} from "@hono-rate-limiter/cloudflare";

// Re-export for Cloudflare to find the DO class
export { DurableObjectRateLimiter };

type Env = {
  RATE_LIMITER: DurableObjectNamespace<DurableObjectRateLimiter>;
};

const app = new Hono<{ Bindings: Env }>();

app.use(async (c, next) => {
  const limiter = rateLimiter({
    windowMs: 60_000,
    limit: 100,
    keyGenerator: (c) => c.req.header("cf-connecting-ip") ?? "",
    store: new DurableObjectStore({
      namespace: c.env.RATE_LIMITER,
    }),
  });
  return limiter(c, next);
});

export default app;

Configuration Options

new DurableObjectStore({
  namespace: doNamespace, // Required: DO namespace binding
  prefix: "hrl:",         // Optional: Key prefix (default: "hrl:")
});

How It Works

The DurableObjectRateLimiter class extends Cloudflare's DurableObject:

  1. Each unique key maps to a Durable Object instance
  2. Hit counts are stored in the DO's transactional storage
  3. An alarm is set to reset the count when the window expires
  4. Operations are strongly consistent within each DO

Limitations

  • Higher latency: Single point of coordination per key
  • Complex setup: Requires migrations and class exports
  • Higher cost: More expensive for high-traffic scenarios