· cloudflare / workers / durable-objects

Cách dùng Cloudflare Durable Objects — hướng dẫn thực chiến

Durable Objects cho bạn compute đơn luồng, duy nhất toàn cầu, đặt cùng vị trí SQLite. Không cần lớp phối hợp, không race condition. Đây là cách kết nối.

Bởi · Cập nhật 8 tháng 6, 2026

2.527 từ · 13 phút đọc

State phân tán khó vì các lần ghi đồng thời cần phối hợp. Cloudflare Durable Objects bỏ qua hoàn toàn lớp phối hợp đó: mỗi object là một process đơn luồng với SQLite database riêng, được cấp phát gần người dùng và duy nhất trên toàn cầu. Bạn không cần lock; không thể xảy ra race condition vì chỉ có đúng một instance chạy tại một thời điểm.

Hướng dẫn này đi qua từng bước để kết nối một Durable Object sẵn sàng cho production — định nghĩa class, binding trong wrangler.toml, gọi từ Worker, SQLite storage, và WebSocket Hibernation API giúp giảm chi phí real-time điển hình từ $138/tháng xuống còn khoảng $10.

Dành cho ai

Các nhà phát triển Cloudflare Workers cần state được phối hợp theo từng entity — phòng chat trực tiếp, counter theo người dùng, game session, rate limiter. Bạn nên quen với TypeScript và đã cài Wrangler v3.

Bạn sẽ xây dựng gì

Cuối hướng dẫn này, bạn sẽ có một TypeScript Durable Object được kết nối với Worker, đọc và ghi dữ liệu SQLite theo cách atomic, kèm Hibernation API sẵn sàng để kiểm soát chi phí WebSocket. Tất cả năm bước đều chạy được cục bộ với wrangler dev trước khi bạn triển khai lên production.

Durable Objects là gì (và khi nào nên dùng)

Durable Object là một loại Cloudflare Worker class đặc biệt. Mỗi instance được đặt tên chạy ở đúng một vị trí tại một thời điểm, bất kỳ đâu trên mạng của Cloudflare, và mọi request gửi đến đều được xử lý tuần tự. Hãy nghĩ nó như một JavaScript process chạy trong một tab trình duyệt: đơn luồng, cooperative multitasking, không chia sẻ bộ nhớ với bất kỳ thứ gì khác.

Các thuộc tính chính:

  • Đơn luồng. Các request xếp hàng và thực thi lần lượt. Không cần locking, không cần vòng lặp compare-and-swap.
  • Duy nhất toàn cầu. "room-123" là cùng một object, trên cùng một máy, dù request đến từ đâu.
  • Compute và storage cùng vị trí. SQLite database nhúng sẵn nằm trên cùng máy với code. Đọc dữ liệu nhanh hơn một mili giây vì không có network hop.
  • Tự động hibernate. Sau khoảng 10 giây không hoạt động, object ngừng tính phí. Nó sẽ khởi động lại trong suốt khi có request tiếp theo. Eviction hoàn toàn xảy ra sau 70–140 giây; storage lâu bền sẽ không mất.
  • Không thể truy cập trực tiếp từ internet. Bạn truy cập DO thông qua một Worker stub. Không có gì trên internet công cộng có thể địa chỉ hóa nó trực tiếp — Worker của bạn là cửa vào duy nhất.

Mô hình single-writer là điểm cốt lõi. Bạn có strong consistency mà không cần distributed lock manager, vì bản thân object chính là cái lock. Mọi request thay đổi dữ liệu đều chờ đến lượt trong một hàng đợi tuần tự.

Khi nào nên dùng Durable Object

Trường hợp sử dụngTại sao DO phù hợp
Chỉnh sửa cộng tác real-timeSingle writer loại bỏ merge conflict
Phòng chat / presenceMột phòng = một DO; danh sách thành viên luôn nhất quán
State game multiplayerGame state nằm trong một instance; không cần sync giữa các shard
Rate limiting theo người dùngCập nhật counter atomic, không cần vòng lặp CAS
Hub pub-sub trực tiếpDanh sách subscriber luôn nhất quán

Câu hỏi quyết định: nhiều client có cần ghi vào cùng một phần state và thấy thay đổi của nhau ngay lập tức không? Nếu có, DO là công cụ phù hợp. Nếu bạn chỉ cần cache một lookup thay đổi mỗi ngày một lần, Workers KV rẻ hơn và đơn giản hơn.

Ví dụ triển khai rate limiting với Durable Objects trong thực chiến: Cách rate-limit Cloudflare Workers không cần Redis.

DO so với KV và R2

Durable ObjectsWorkers KVR2
Tính nhất quánNhất quán mạnh (per-object)Nhất quán cuối cùngNhất quán cuối cùng (tương thích S3)
Throughput ghiTuần tự theo object1 lần ghi/giây mỗi keyCao (upload blob)
Độ trễ đọcDưới 1 ms (cùng vị trí)500µs–10ms (key nóng được cache)Cao hơn
Mô hình dữ liệuBảng SQLite + key-valueFlat key-valueBlob không có cấu trúc
Compute cùng vị tríKhôngKhông
Dùng choState phối hợp theo entityCấu hình toàn cầu, feature flagFile, media, backup

Giới hạn 1 lần ghi/giây trên một key đơn của KV là ràng buộc thực sự. Nếu bạn xây dựng một counter mà hàng nghìn người dùng tăng đồng thời, KV sẽ bị nghẽn. DO tuần tự hóa các lần ghi nhưng không bao giờ từ chối chúng.

Nếu bạn cần SQL database serverless chia sẻ giữa nhiều Worker thay vì storage per-object, xem Cloudflare D1 năm 2026: đã sẵn sàng cho production chưa?.

Bước 1: Viết DO class

Tạo src/my-do.ts. Class của bạn extend DurableObject<Env> — generic này về kỹ thuật là tùy chọn nhưng cho phép truy cập env với type safety. Import từ "cloudflare:workers", không phải từ bất kỳ npm package nào.

// src/my-do.ts
import { DurableObject } from "cloudflare:workers";

export class MyDurableObject extends DurableObject<Env> {
  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env);
  }

  async fetch(request: Request): Promise<Response> {
    // Requests are serialized — no locking needed
    const count = (await this.ctx.storage.get<number>("count")) ?? 0;
    await this.ctx.storage.put("count", count + 1);
    return new Response(`Count: ${count + 1}`);
  }
}

this.ctx.storage là key-value storage API. Bạn sẽ chuyển sang this.ctx.storage.sql ở Bước 4 cho các truy vấn relational. Cả hai API đều dùng chung một ctx.storage object — chúng là hai giao diện khác nhau trên cùng một SQLite database của mỗi object instance.

Lỗi thường gặp: Import từ đường dẫn sai sẽ gây TypeError: DurableObject is not a constructor khi chạy. Import đúng là chuỗi ký tự "cloudflare:workers" — không có npm package nào tên đó và @cloudflare/workers-types chỉ là package định nghĩa type.

Bước 2: Khai báo binding trong wrangler.toml

Mở (hoặc tạo) wrangler.toml. Bạn cần hai stanza: một binding đặt tên cho DO và trỏ đến class đã export, và một migration để cấp phát SQLite backend.

name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-09-23"

[[durable_objects.bindings]]
name = "MY_DO"                      # how your Worker references it (env.MY_DO)
class_name = "MyDurableObject"      # exported class name in src/my-do.ts

[[migrations]]
tag = "v1"
new_sqlite_classes = ["MyDurableObject"]

new_sqlite_classes là cách khuyến nghị hiện tại cho tất cả DO class mới. Nó cấp phát một SQLite database riêng cho mỗi object instance và hoạt động trên gói Free.

Lỗi hay gặp: dùng new_classes thay vì new_sqlite_classes sẽ tạo ra một object dùng KV làm backend. KV-backed DO yêu cầu gói Paid và không có SQLite storage API. Nhiều tutorial cũ và code do AI tạo ra vẫn dùng new_classes — đừng sao chép cho dự án mới. Hiện không có con đường migration chính thức từ KV-backed sang SQLite-backed nếu bạn đã triển khai với new_classes.

Lỗi thường gặp: Bỏ stanza [[migrations]] khiến Wrangler từ chối triển khai: Error: A migration is required to use Durable Objects. Thêm nó trước lần wrangler deploy đầu tiên. Chạy wrangler types sau khi thêm binding để sinh ra worker-configuration.d.ts — nếu không, TypeScript sẽ không biết về env.MY_DO.

Bước 3: Gọi DO từ một Worker

Thêm export của DO vào entry point chính của Worker và kết nối routing.

// src/index.ts
import { MyDurableObject } from "./my-do";

export { MyDurableObject };  // re-export so Wrangler registers the class

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // idFromName: same string → same DO instance, anywhere in the world
    const id = env.MY_DO.idFromName("room-123");
    const stub = env.MY_DO.get(id);

    // RPC: forward the request to the DO
    return stub.fetch(request);
  },
};

idFromName("room-123") mang tính quyết định — nó luôn ánh xạ đến cùng một object. Dùng nó cho các entity được đặt tên: phòng chat, user session, game lobby. Với các object ẩn danh dùng một lần, dùng env.MY_DO.newUniqueId() thay thế.

Với truy cập DO từ Worker khác — gọi một DO được định nghĩa trong Worker khác — thêm script_name = "other-worker" vào binding block trong wrangler.toml. Stub API là như nhau; chỉ có khai báo binding thay đổi.

Các benchmark từ cộng đồng ước tính không chính thức khoảng 500–1.000 request mỗi giây mỗi object là ngưỡng throughput thực tế (không có SLA chính thức). Với hầu hết các trường hợp per-entity, đó là biên độ mà bạn sẽ không bao giờ chạm tới. Nếu bạn kỳ vọng một object đơn lẻ xử lý hàng triệu request mỗi giây, hãy mô hình hóa vấn đề theo hướng khác — phân mảnh theo sub-ID của entity, hoặc cân nhắc liệu bạn có thực sự cần serialization ở quy mô đó.

Lỗi thường gặp: Quên re-export DO class từ src/index.ts gây ra Class not found lỗi runtime. Wrangler giải quyết class tại Worker entry point được chỉ định bởi main — re-export phải có ở đó, dù class nằm ở file khác.

Bước 4: Dùng SQLite storage

Key-value API hoạt động tốt cho các trường hợp đơn giản. Với truy vấn relational hoặc atomicity trên nhiều key, dùng ctx.storage.sql. Các thao tác là đồng bộ — không cần await — và atomic trong phạm vi một lần gọi handler.

// Inside MyDurableObject
async incrementWithSQL(): Promise<number> {
  // CREATE TABLE is idempotent — safe to call on every request
  this.ctx.storage.sql.exec(
    `CREATE TABLE IF NOT EXISTS counters (
       key   TEXT PRIMARY KEY,
       count INTEGER NOT NULL DEFAULT 0
     )`
  );

  const result = this.ctx.storage.sql.exec<{ count: number }>(
    `INSERT INTO counters (key, count) VALUES (?, 1)
     ON CONFLICT(key) DO UPDATE SET count = count + 1
     RETURNING count`,
    "my-key"
  );

  return result.one().count;
}

Mỗi DO instance có SQLite database riêng — không object nào khác có thể đọc hoặc ghi vào đó. Giới hạn storage là 10 GB mỗi object. Con số cũ “1 GB” xuất hiện trong tài liệu beta; nó đã được nâng lên 10 GB khi SQLite trong Durable Objects GA vào tháng 4 năm 2025. Đừng trích dẫn giới hạn cũ.

Tính phí SQLite trên gói Paid bắt đầu vào tháng 1 năm 2026. Các tháng từ khi GA vào tháng 4 năm 2025 đến tháng 1 năm 2026 là giai đoạn miễn phí tính phí ngay cả trên gói Paid.

Lỗi thường gặp: Gọi ctx.storage.sql.exec trước khi bảng tồn tại sẽ crash với SQLite error: no such table. Gọi CREATE TABLE IF NOT EXISTS ở đầu bất kỳ method nào truy vấn bảng, hoặc khởi tạo schema trong constructor. Guard IF NOT EXISTS đảm bảo an toàn khi chạy mỗi lần gọi mà không ảnh hưởng đến hiệu năng.

Bước 5: WebSocket Hibernation

Nếu DO của bạn quản lý kết nối WebSocket — chat, multiplayer, live presence — hãy dùng Hibernation API. Không có nó, DO sẽ tính phí cho toàn bộ thời gian của mọi kết nối đang mở, kể cả khi không hoạt động. Với nó, object hibernate giữa các tin nhắn và chỉ tính phí khi thực sự xử lý.

Ví dụ giá chính thức: ~$138/tháng không có Hibernation so với ~$10/tháng có Hibernation cho cùng một workload WebSocket.

// src/chat-room.ts
import { DurableObject } from "cloudflare:workers";

export class ChatRoom extends DurableObject<Env> {
  async fetch(request: Request): Promise<Response> {
    const [client, server] = Object.values(new WebSocketPair());
    // acceptWebSocket hands the socket to the Hibernation runtime
    this.ctx.acceptWebSocket(server);
    return new Response(null, { status: 101, webSocket: client });
  }

  // Called when a message arrives — object may have been hibernating
  async webSocketMessage(ws: WebSocket, message: string): Promise<void> {
    for (const socket of this.ctx.getWebSockets()) {
      socket.send(message);
    }
  }

  async webSocketClose(ws: WebSocket): Promise<void> {
    ws.close();
  }
}

Điểm khác biệt chính so với WebSocket API tiêu chuẩn: thay vì ws.accept(), bạn gọi this.ctx.acceptWebSocket(server). Điều này chuyển socket cho hibernation manager của runtime. Khi có tin nhắn đến, runtime thức dậy DO và gọi webSocketMessage.

Hibernation API hiện chỉ được tài liệu hóa cho WebSocket. Kết nối SSE và HTTP streaming không được hỗ trợ — nếu bạn giữ một kết nối SSE lâu dài, object sẽ luôn thức suốt thời gian đó.

Lỗi thường gặp: Dùng ws.accept() (con đường không hibernate) giữ DO thức suốt vòng đời của mọi kết nối. Bạn sẽ thấy hóa đơn CPU phản ánh mỗi giây mỗi socket đang mở. Chuyển sang ctx.acceptWebSocket() và triển khai webSocketMessagewebSocketClose như các class method — cả hai handler đều bắt buộc để hibernation hoạt động. Bỏ qua một trong hai khiến runtime tự động quay về chế độ luôn bật mà không có cảnh báo.

Giá cả

Durable Objects với SQLite storage có sẵn trên gói Workers Free:

Giới hạnGói Free
Request100.000 / ngày
SQLite storage5 GB tổng cộng
Lần đọc hàng5.000.000 / ngày
Lần ghi hàng100.000 / ngày

Backend storage KV cũ (new_classes trong migrations) yêu cầu gói Paid. Với dự án mới, new_sqlite_classes rẻ hơn để bắt đầu và cho bạn toàn bộ SQLite API.

Trên gói Paid, tính phí theo request, theo GB-month storage, và theo triệu lần đọc/ghi hàng. Kiểm tra trang giá hiện tại để có con số chính xác — chi phí đơn vị đã thay đổi kể từ khi tính phí bắt đầu vào tháng 1 năm 2026.

Hiện không có quan hệ affiliate giữa toolchew và Cloudflare. CTA bên dưới là tự nhiên.

Bước tiếp theo

Bắt đầu cục bộ — wrangler dev chạy DO của bạn trong một SQLite sandbox cục bộ. Không cần tài khoản Cloudflare cho đến khi bạn sẵn sàng triển khai.

Nếu bạn chưa quen với quy trình deploy của Cloudflare Workers, Cách deploy Cloudflare Worker với D1 + Stripe Webhooks bao quát toàn bộ bước từ scaffold đến production.

Tham khảo cho production: PartyKit, nay là một phần của Cloudflare, đã xây toàn bộ hạ tầng cộng tác real-time trên Durable Objects và đã công bố kiến trúc đó. OneUptime đã chuyển nền tảng quản lý sự cố của họ sang DO vào tháng 1 năm 2026 và ghi lại quá trình, bao gồm cả các đánh đổi xung quanh throughput của object.

Pattern “database-per-user” với SQLite DO đáng để khám phá nếu bạn đang xây dựng ứng dụng multi-tenant — mỗi người dùng có một SQLite database riêng biệt, không chia sẻ schema giữa các tenant, với chi phí hạ tầng gần như bằng không trên mỗi người dùng.

Đăng ký tài khoản Cloudflare miễn phí và chạy wrangler deploy khi bạn sẵn sàng. Gói Free là đủ để đưa một tính năng real-time lên production.