· cloudflare / workers / rate-limiting

Cách rate-limit Cloudflare Workers không cần Redis

Rate-limit Cloudflare Workers thuần native — không cần Redis. Rate Limiting API chặn abuse, KV cho quota hàng ngày, Durable Objects đếm chính xác cho billing.

Bởi

2.189 từ · 11 phút đọc

Nếu bạn cần rate limiting trên Cloudflare Workers, bạn không cần Redis. Không cần Upstash. Cloudflare có sẵn ba primitive bao phủ toàn bộ phổ từ “chặn bot script-kiddie” đến “đếm từng request cho hệ thống billing.” Chọn đúng cái cho use case của bạn và bạn xong trong vòng chưa đầy một tiếng.

Bài này dành cho ai

Dành cho developers làm việc với Workers, muốn rate limiting mà không cần kéo thêm một cache layer bên ngoài. Nếu bạn đã có Redis trong stack và đang dùng ổn, bỏ qua bài này — thêm một Workers binding không tiết kiệm được nhiều đâu. Nếu Redis vẫn là lựa chọn cho phần khác trong stack, xem thêm Redis vs Valkey 2026.

Để so sánh Cloudflare Workers với các nền tảng khác trước khi quyết định, xem Cloudflare Workers vs AWS LambdaCloudflare Workers vs Vercel Functions.

Môi trường thử nghiệm

Cả ba cách đều chạy trên Wrangler 4.94.0 (cập nhật tại thời điểm 2026-05-27). Cần Wrangler ≥ 4.36.0 cho Rate Limiting API. Workers runtime là V8 isolate tiêu chuẩn của Cloudflare. Giới hạn Free plan được áp dụng xuyên suốt — chỗ nào Paid plan thay đổi con số, bài có ghi rõ.

Rate Limiting API

Bắt đầu từ đây. Khả dụng trên tất cả các plan kể cả Free. Không cần thêm bất kỳ infrastructure nào. Latency phát sinh: không có — counter nằm ngay trên cùng máy với Worker của bạn.

API này đạt general availability vào ngày 2025-09-19. Nó dùng thuật toán sliding window dựa trên memcached, cùng hệ thống đứng sau WAF rate limiting của Cloudflare — xử lý hàng tỷ request mỗi ngày với error rate đo được là 0.003%.

Một block [[ratelimits]] trong wrangler.toml, một method call trong handler:

# wrangler.toml
[[ratelimits]]
name = "MY_RATE_LIMITER"
namespace_id = "1001"          # số nguyên dương bất kỳ, unique per account

  [ratelimits.simple]
  limit = 100                  # số request cho phép mỗi period
  period = 60                  # 10 hoặc 60 giây — chỉ có hai lựa chọn
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const userId = request.headers.get("x-user-id") ?? "anonymous";

    const { success } = await env.MY_RATE_LIMITER.limit({ key: userId });

    if (!success) {
      return new Response("429 Rate limit exceeded", { status: 429 });
    }
    return new Response("OK");
  },
};

Bạn có thể phân biệt user free và paid bằng hai binding riêng:

[[ratelimits]]
name = "FREE_RL"
namespace_id = "1001"
[ratelimits.simple]
limit = 100
period = 60

[[ratelimits]]
name = "PAID_RL"
namespace_id = "1002"
[ratelimits.simple]
limit = 1000
period = 60
const limiter = isPaidUser ? env.PAID_RL : env.FREE_RL;
const { success } = await limiter.limit({ key: userId });

Latency: Việc cập nhật counter là synchronous ngay trên máy local, không phải một network call. Cloudflare mô tả đây là không thêm latency đáng kể vì memcached lookup xảy ra trong cùng PoP với isolate, trước khi Worker trả về.

Điểm hạn chế: Giới hạn là per Cloudflare PoP, không phải global. Một user có request phân tán qua hai location của Cloudflare sẽ nhận được 100 request mỗi phút ở mỗi location — tổng là 200, không phải 100. Để chặn abuse thì ổn. Để dùng cho billing thì không được. Docs ghi rõ: đây được “thiết kế cố ý để không dùng như một hệ thống kế toán chính xác.”

Thêm hai ràng buộc khác cần biết trước:

  • Period window chỉ có 10 giây hoặc 60 giây. Không có window 5 phút, không có giới hạn theo giờ.
  • IP-based key không được khuyến nghị — mobile NAT và corporate proxy gom nhiều user sau một IP. Ưu tiên user ID hoặc API key làm giá trị key.

Workers KV

Dùng cho daily quota và soft cap. Eventually consistent, replicate toàn cầu, đọc rẻ. Độ trễ propagation global 60 giây là đặc điểm cơ bản của nó — không phải lỗi, mà là thiết kế.

Use case điển hình: bạn có giới hạn plan “500 AI call mỗi ngày.” Đếm chính xác qua các concurrent request không quan trọng lắm vì user đụng vào daily cap thường đã gần cạn quota. Vài request thêm lọt qua lúc nửa đêm UTC là chấp nhận được. Vài request thêm lọt qua rate limit mỗi giây thì không.

# wrangler.toml
[[kv_namespaces]]
binding = "KV"
id = "<your-kv-namespace-id>"
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const userId = request.headers.get("x-user-id") ?? "anonymous";
    // Daily key — một row mỗi user mỗi ngày UTC
    const key = `quota:${userId}:${new Date().toISOString().slice(0, 10)}`;

    const raw = await env.KV.get(key);
    const count = raw ? parseInt(raw, 10) : 0;
    const DAILY_LIMIT = 500;

    if (count >= DAILY_LIMIT) {
      return new Response("429 Daily quota exceeded", { status: 429 });
    }

    // Hết hạn sau 24 giờ bất kể ngày bắt đầu từ lúc nào
    await env.KV.put(key, String(count + 1), { expirationTtl: 86400 });
    return new Response("OK");
  },
};

Latency: Đọc khi đã cache thì nhanh từ edge. Đọc lần đầu (chưa cache hoặc sau khi TTL hết) phải fetch từ central store — latency cao hơn đáng kể so với cache hit. Write acknowledge ngay locally; propagation toàn cầu mất tới 60 giây.

Giới hạn cứng:

  • 1 write mỗi giây mỗi key. Đây là ràng buộc nền tảng của KV. Nếu một user bắn hơn 1 req/s, KV trả về HTTP 429 trên write — không phải response rate limit của bạn, mà là lỗi infrastructure từ KV. Bất kỳ user nào tạo ra hơn 1 req/s đến cùng một key sẽ bắt đầu gặp lỗi này. KV không phù hợp cho per-request rate limiting trên API đang hoạt động.
  • Race condition theo thiết kế. Hai request từ hai PoP khác nhau có thể cùng đọc count = 49, cùng vượt qua kiểm tra, rồi cùng ghi count = 50 với limit là 50. Bạn sẽ phục vụ quá mức. Với daily quota, điều đó chấp nhận được. Với bất cứ thứ gì mà đếm sai tốn tiền (billing, API credit), thì không.

Free plan: 100,000 lượt đọc/ngày, 1,000 lượt ghi/ngày. Giới hạn write làm KV gần như không dùng được cho user nào có traffic đáng kể trên Free — 1,000 lượt ghi tương đương 1,000 user riêng biệt, mỗi người chạm vào quota endpoint đúng một lần. Tính toán kỹ trước khi chọn.

Durable Objects

Dùng cho strict global counter. Strong consistency, chính xác toàn cầu, code phức tạp hơn. Nếu eventually consistency per-PoP của Rate Limiting API là vấn đề và giới hạn 1-write/s của KV loại nó ra, đây là primitive cần dùng.

Durable Objects là các singleton process với quyền truy cập riêng, serialized vào SQLite storage. Route tất cả request của một user đến cùng một named object, và mọi lần tăng counter đều được serialize qua thread đơn của object đó. Không có race condition. Không có over-counting. Mọi request từ mọi nơi đều được đếm đúng một lần.

# wrangler.toml
[[durable_objects.bindings]]
name = "RATE_LIMITER"
class_name = "RateLimiter"

[[migrations]]
tag = "v1"
new_sqlite_classes = ["RateLimiter"]
import { DurableObject } from "cloudflare:workers";

export class RateLimiter extends DurableObject<Env> {
  async check(limit: number, windowSec: number): Promise<boolean> {
    const now = Date.now();
    const windowStart = now - windowSec * 1000;

    this.ctx.storage.sql.exec(`CREATE TABLE IF NOT EXISTS hits (ts INTEGER)`);
    this.ctx.storage.sql.exec(`DELETE FROM hits WHERE ts < ?`, windowStart);

    const { count } = this.ctx.storage.sql
      .exec(`SELECT COUNT(*) as count FROM hits`)
      .one() as { count: number };

    if (count >= limit) return false;

    this.ctx.storage.sql.exec(`INSERT INTO hits VALUES (?)`, now);
    return true;
  }
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const userId = request.headers.get("x-user-id") ?? "anonymous";
    // Một DO mỗi user — getByName route toàn cầu đến cùng một instance
    const stub = env.RATE_LIMITER.getByName(userId);
    const allowed = await stub.check(100, 60); // 100 req/min

    if (!allowed) {
      return new Response("429 Too Many Requests", { status: 429 });
    }
    return new Response("OK");
  },
};

Latency: Request từ cùng Cloudflare PoP với home region của DO thì nhanh sau warmup. Request từ châu lục khác phải trả thêm một cross-region network hop — overhead latency đáng kể. Đó là sự đánh đổi — global consistency tốn một network round-trip với user ở xa DO.

Cold start có thể xảy ra khi object bị evict do không hoạt động. Với API có traffic đều đặn, điều này hiếm. Với traffic bùng phát hoặc không đều, hãy tính đến các spike cold-start.

Không nên dùng một DO global cho toàn bộ traffic. Một object serialize một request một lúc; ở ~500–1,000 req/s nó trở thành điểm nghẽn. Đặt tên object theo từng user: env.RATE_LIMITER.getByName(userId).

Không nên lưu counter trong instance variable. Object instance bị evict khỏi memory sau một thời gian không hoạt động; trạng thái in-memory sẽ mất. Luôn ghi vào ctx.storage.sql — đó là thứ tồn tại qua các chu kỳ hibernation.

Giới hạn theo plan: Free plan cho phép 100,000 DO request/ngày và 13,000 GB-second compute. Production API sẽ đụng cả hai. Paid plan ($5/tháng tối thiểu) nâng giới hạn request lên 1M/tháng và thêm 400,000 GB-second — phù hợp hơn cho traffic liên tục.

Ma trận quyết định

Rate Limiting APIWorkers KVDurable Objects
ConsistencyEventually consistent (per-PoP)Eventually consistent (tới 60 s global)Strong (chính xác toàn cầu)
Latency thêm vào0 ms (cùng máy)Nhanh khi cache; chậm hơn khi cold readCross-region network hop
Tần suất write tối đaKhông có giới hạn ghi chép1 write/s mỗi key~500–1,000 req/s mỗi object
Free plan✓ (có điều kiện)✓ (100K req/ngày)
Cold startKhôngKhôngCó, sau idle
Độ phức tạp codeThấpTrung bìnhCao
Phù hợp cho billingKhôngKhông
Linh hoạt về windowChỉ 10 s hoặc 60 sTùy ý (qua TTL)Tùy ý (qua custom SQL)

Kết luận theo từng loại workload

Hobby / chặn abuse: Rate Limiting API. Hai dòng config, không tốn thêm latency, chạy trên Free. Việc đếm per-PoP là ổn — bạn đang chặn bot, không phải đếm API credit.

Production API với soft quota (giới hạn hàng ngày hoặc hàng tuần): Workers KV, với lưu ý là user bắn hơn 1 req/s sẽ bắt đầu gặp lỗi KV write. Kết hợp KV quota check với một Rate Limiting API binding để bắt burst traffic trước khi nó đụng vào quota write.

Production API với billing chặt chẽ hoặc tính credit theo user: Durable Objects. Chấp nhận overhead latency cross-region; đó là cái giá của global accuracy. Nâng lên Paid plan ($5/tháng) trước khi chạm giới hạn Free 100K req/ngày — bạn sẽ chạm nhanh hơn bạn nghĩ trên bất kỳ API thực tế nào.

Enterprise / multi-region strict: Durable Objects trên Paid plan với named object per user. Nếu throughput mỗi user vượt ~500 req/s, shard theo tiền tố user ID — dù ở tầm đó bạn chắc đã có infra people trong team rồi.

Điểm cần lưu ý

Behavior per-PoP của Rate Limiting API là đặc điểm bị hiểu nhầm nhiều nhất. Mỗi Cloudflare location duy trì counter riêng độc lập. Một client luân phiên qua nhiều PoP (CDN, mobile carrier có anycast, VPN) có thể vượt giới hạn danh nghĩa mà API không phát hiện được. Đây là hành vi được ghi trong tài liệu, không phải lỗi — hãy điều chỉnh giới hạn của bạn tương ứng.

Độ trễ replication 60 giây của Workers KV có nghĩa là sau khi bạn ban một user hoặc họ hết quota, họ có thể tiếp tục gửi request từ một PoP khác trong tối đa một phút. Với daily quota, đây là mức nhiễu chấp nhận được. Với giới hạn nhạy cảm về bảo mật, thì không.

Durable Objects cold start có thể dự đoán được nhưng không loại bỏ được. Nếu API của bạn có khoảng thời gian idle dài giữa các đợt burst, hãy tính đến cold-start penalty cho request đầu tiên của mỗi đợt. Không có cách nào giữ DO luôn warm trên Free plan mà không tạo traffic giả.

Không cách nào trong số này thay thế được WAF-level rate limiting của Cloudflare nếu bạn cần bảo vệ trước khi traffic đụng vào Worker. WAF rule kích hoạt ở edge trước khi isolate khởi động.

Để xem phân tích chi phí Cloudflare ở quy mô lớn — bao gồm Workers, R2, D1 so với AWS — xem Cloudflare vs AWS: phân tích chi phí đầy đủ.

Tài liệu tham khảo

  1. Rate Limiting API docs (GA)
  2. Rate Limiting GA changelog — September 19, 2025
  3. Cloudflare sliding window algorithm
  4. Workers KV — how it works
  5. KV write limits
  6. Durable Objects overview
  7. Durable Objects pricing
  8. Durable Objects best practices