# Cloudflare Worker phục vụ site tĩnh: hai đường tới cùng một file, lùi phiên bản 6 giây và trần 50 subrequest

Ba điều cần biết khi đặt site tĩnh và API lên Cloudflare Worker: hai đường tới cùng một file có thể lệch sau deploy, lùi phiên bản mất 6 giây, và trần 50 subrequest của gói Free.

Published: 2026-10-12 · Language: vi · Tags: Cloudflare Workers, Edge Computing, vận hành, deploy · Canonical: https://hecigo.com/blog/cloudflare-worker-phuc-vu-site-tinh-hai-duong-lui-phien-ban-va-tran-50-subrequest/

---

Trên Cloudflare Worker có static assets, cùng một file có thể tới tay người gọi bằng hai lối: qua mã Worker, hoặc thẳng từ asset server. Trong vài giây đầu sau `wrangler deploy`, hai lối đó có thể trả hai phiên bản khác nhau. Bài này gom ba điều đáng biết khi phục vụ site tĩnh và API trên Worker, kèm số đo từ hecigo.com và Ads Insights: hai đường lệch nhau sau deploy, lùi phiên bản mất 6 giây, và trần 50 subrequest của gói Free.

## Worker và asset server: ai trả lời request nào

Một Worker có static assets gồm hai phần: **asset server** trả file trong thư mục build, và **mã Worker** của bạn. Mặc định asset server đi trước, request khớp đúng một file thì mã Worker không chạy. Request tới static assets miễn phí và không giới hạn ở mọi gói; chỉ request làm mã Worker chạy mới bị tính.

hecigo.com là bản build Next.js tĩnh cộng một Worker đọc header `Accept` để trả HTML hay Markdown cho cùng một URL. Worker phải chạy trước asset server mới thấy header đó:

```jsonc
"assets": {
  "directory": "./out",
  "binding": "ASSETS",
  "html_handling": "auto-trailing-slash",
  "not_found_handling": "none",
  "run_worker_first": ["/", "/*/"]
}
```

Bản rút gọn, file thật liệt kê thêm các đường dẫn chuyển hướng. Hai điểm đáng chú ý:

- **`run_worker_first` là một hình dạng URL, không phải danh sách.** Site bật `trailingSlash` nên mọi URL trang kết thúc bằng `/`, còn không file nào như vậy. Ảnh, font và cả bó `/_next/static/` không bao giờ chạm tới Worker. Đặt `true` vẫn chạy đúng, nhưng mỗi ảnh thành một lần gọi bị tính.
- **`not_found_handling: "none"`** để đường dẫn không có file rơi xuống Worker dưới dạng 404 trơn, Worker tự chọn trả lỗi bằng JSON, HTML hay Markdown. Đặt `"404-page"` thì asset server trả trang HTML trước khi Worker kịp quyết.

## Hai đường tới cùng một file có thể lệch sau deploy

| Request | Ai trả lời |
|---|---|
| `/blog/` + `Accept: text/markdown` | Worker chạy, gọi `env.ASSETS.fetch('/blog.md')` |
| `/blog.md` | Asset server trả thẳng |

Trong hai tới bốn giây đầu sau deploy, hai lối này chưa chắc cùng thấy phiên bản mới, và lối chậm hơn trả bản cũ. Với người đọc trang, không có gì hỏng. Với agent đọc site bằng Markdown, hỏi bằng header và hỏi bằng đuôi `.md` cho hai kết quả khác nhau.

**Purge cache không giải quyết được.** `cf-cache-status: HIT` trên `/blog.md` đến từ tầng static assets của Workers, không phải cache HTTP của zone, và tầng đó khoá theo nội dung: `/blog.md?x=<chuỗi ngẫu nhiên>` vẫn HIT, cùng `etag`. Purge theo URL không có gì để xoá.

**Cho cổng kiểm thử lại khi lệch cũng không phải cách.** Một cổng biết thử lại là cổng đã học cách chấp nhận lệch.

Cách hecigo dùng: một bước chờ trước cổng kiểm, hỏi xem **cả hai đường đã trả đúng từng byte của bản build chưa**. Không so hai bên với nhau, vì hai bên cùng cũ cũng giống nhau.

```js
// Rút gọn từ waitForDeploy trong scripts/wait-for-deploy.mjs: bỏ phần đọc out/, in báo cáo và xác thực staging.
const TIMEOUT_MS = 180_000
const INTERVAL_MS = 5_000
const STABLE_ROUNDS = 2      // một lượt sạch có thể là may

let clean = 0
const deadline = Date.now() + TIMEOUT_MS
while (clean < STABLE_ROUNDS) {
  if (Date.now() > deadline) throw new Error('quá trần, còn đường trả bản cũ')
  // checkRound: mọi trang, cả hai đường, so với từng byte trong out/; trả danh sách đường còn lệch
  const stale = await checkRound({ fetchImpl: fetch, origin, pages, expected })
  clean = stale.length === 0 ? clean + 1 : 0
  if (clean < STABLE_ROUNDS) await sleep(INTERVAL_MS)
}
```

Phải sạch hai lượt liền nhau vì các máy trong cùng một điểm hiện diện không nhận phiên bản mới cùng lúc, và cổng kiểm chạy ngay sau đó bắn hơn 300 request. Lỗi 5xx và lỗi mạng tính là chưa sẵn sàng, không tính là hỏng. Ở một lần đăng bài đã đo, bước chờ mất 18 giây.

> Related: [Đổi host và bốn hành vi nền tảng đang làm hộ mà bạn không sở hữu](https://hecigo.com/blog/doi-host-va-bon-hanh-vi-nen-tang-lam-ho-ma-ban-khong-so-huu/): Chuyển hecigo.com sang một host khác. Adapter mất mười lăm dòng. Bốn hành vi nền tảng vẫn làm hộ thì mỗi cái hỏng một kiểu, và không cái nào làm...

## Lùi phiên bản: asset có lùi theo

Mỗi lần deploy là một phiên bản Worker mới. Lùi là đổi phiên bản đang phục vụ, không đụng DNS:

```bash
npx wrangler versions list
npx wrangler rollback <version-id>
```

Cloudflare cho lùi về tối đa 100 phiên bản gần nhất, chặn lùi nếu phiên bản đích bind tới R2, KV hay queue đã bị xoá, và wrangler cảnh báo rằng nó không lùi tài nguyên được bind (Durable Object, D1, R2, KV).

Điều tài liệu không nói thẳng: **static assets có lùi theo phiên bản không?** Nếu không, mã Worker cũ sẽ phục vụ asset mới, một tổ hợp chưa từng tồn tại ở lần build nào. hecigo kiểm bằng diễn tập trên production:

| Số đo | Kết quả |
|---|---|
| Asset lùi theo phiên bản | có, 6 giây sau lệnh lùi |
| Cổng hợp đồng khi ở phiên bản cũ | 452 phép kiểm, 0 hỏng |
| Cổng hợp đồng sau khôi phục | 452 phép kiểm, 0 hỏng |
| Production ở phiên bản cũ | khoảng 26 giây |

Diễn tập là một workflow GitHub Actions chạy tay, dùng chung hàng đợi với workflow deploy: lùi về phiên bản liền trước, chạy cổng hợp đồng, khôi phục bằng `wrangler deploy`, chờ hai đường khớp bản build, chạy cổng lần nữa. Kết quả chỉ đúng cho cách deploy lúc chạy, nên đổi cách build là chạy lại.

## Trần 50 subrequest của gói Free

Ads Insights, MCP server đọc số quảng cáo của hecigo, cũng chạy trên Worker. Mỗi câu hỏi của trợ lý AI thành nhiều lời gọi tới API của Meta, Google và TikTok.

Gói Workers Free giới hạn **50 subrequest ra ngoài cho mỗi lần gọi Worker** (gói trả phí mặc định 10.000). Mọi `fetch` đều tính, kể cả thử lại, làm mới token và từng bước trong chuỗi chuyển hướng. Vượt trần thì lần gọi chết với `Too many subrequests`, mất cả câu trả lời, kể cả phần của tài khoản đã có số.

Con số 50 hết nhanh hơn tưởng. TikTok chỉ nhận tối đa 30 ngày mỗi request khi báo cáo theo ngày, nên báo cáo 400 ngày là 14 cửa sổ cộng một lời gọi lấy đơn vị tiền tệ: **15 lời gọi cho một nhà quảng cáo**. Bốn nhà quảng cáo là 60.

Ads Insights giữ trần bằng một ngân sách cho mỗi lần gọi công cụ, mặc định 45, đọc từ cấu hình:

```ts
// Rút gọn từ src/core/budget.ts của Ads Insights: bỏ phần đặt trước và trả lại.
export class SubrequestBudget {
  private used = 0
  constructor(readonly limit: number) {}

  take(provider: string): void {
    if (this.used >= this.limit) {
      throw new SubrequestBudgetError(provider,
        'ask a shorter range, or one account at a time (accountId)')
    }
    this.used++   // gọi trước MỖI lần fetch, kể cả thử lại và làm mới token
  }
}
```

- **Đặt trước.** Báo cáo TikTok biết trước số lời gọi cần, nên giữ đủ số đó trước lời gọi đầu tiên; không đủ thì từ chối nguyên tài khoản đó, thay vì chạy dở.
- **Chừa chỗ.** TikTok không được lấy một phần mười cuối ngân sách, để dành cho tài khoản Meta hay Google chỉ cần một hai lời gọi.
- **Từ chối bằng câu đọc được.** Trợ lý nhận lời khuyên hỏi khoảng ngày ngắn hơn hoặc từng tài khoản, các tài khoản khác vẫn trả số.

45 chứ không 50 vì sau câu trả lời đầu tiên Worker còn gửi vài sự kiện đo lường. Lên gói trả phí chỉ đổi một biến cấu hình. Lớp này dựng từ việc đếm lời gọi, chưa phải từ sự cố: hecigo chưa thấy `Too many subrequests` nào trong log production của Ads Insights.

## Câu hỏi tự kiểm

- Đường dẫn nào thật sự cần mã Worker, và phạm vi `run_worker_first` của bạn là một hình dạng hay một danh sách sẽ dài dần?
- Có file nào tới được bằng hai lối? Cổng kiểm sau deploy so cả hai với bản build, hay chỉ so hai bên với nhau?
- Lần diễn tập lùi gần nhất có sau lần đổi cách build gần nhất không?
- Lời gọi công cụ đắt nhất của bạn làm bao nhiêu `fetch`, tính cả thử lại? Vượt trần thì hỏng một phần hay hỏng cả câu trả lời?

Câu hecigo chưa có đáp án: bước chờ chỉ che khoảng lệch cho cổng kiểm, agent hỏi đúng vào mấy giây ấy vẫn thấy hai bản khác nhau. Worker có cách nào biết mình đang ở giữa hai phiên bản không?

> **Đang chuyển site hay API lên Cloudflare Worker?** Câu hỏi về phạm vi `run_worker_first`, cổng kiểm sau deploy hay ngân sách subrequest, [nhắn hecigo](https://hecigo.com/#contact). Muốn xem một MCP server chạy trên Worker thì thêm `https://ads.hecigo.com/mcp` vào trợ lý AI của bạn theo [trang hướng dẫn](https://ads.hecigo.com/guide).

> Related: [MCP server là gì: mổ một MCP server đang chạy thật, từ 3 công cụ lên 9 và 18 KB mô tả trợ lý phải đọc](https://hecigo.com/blog/mcp-server-la-gi-nhin-tu-mot-server-dang-chay-that/): MCP server là gì, giải thích bằng một server đang chạy: hai đường truyền, danh sách công cụ trợ lý đọc, kết quả trả về, đăng nhập OAuth và ba chỗ...

## Nguồn tham khảo

- [Pricing](https://developers.cloudflare.com/workers/platform/pricing/) - Cloudflare Workers docs
- [Run your Worker script first](https://developers.cloudflare.com/workers/static-assets/routing/worker-script/) - Cloudflare Workers docs
- [Rollbacks](https://developers.cloudflare.com/workers/versions-and-deployments/rollbacks/) - Cloudflare Workers docs
- [Limits: Subrequests](https://developers.cloudflare.com/workers/platform/limits/#subrequests) - Cloudflare Workers docs
- [Workers are no longer limited to 1000 subrequests](https://developers.cloudflare.com/changelog/post/2026-02-11-subrequests-limit/) - Cloudflare Changelog

---

Published by hecigo, middleware & integration lab. https://hecigo.com · hi@hecigo.com
