Hai năm trước mình từng trả lời “không” khi một client hỏi có chạy được Express API hiện có của họ trên Cloudflare Workers không. Flag nodejs_compat lúc đó đã tồn tại nhưng cảm giác nửa vời — đủ polyfill để demo, chưa đủ để tin tưởng đưa vào production. Ngày 9/9, Cloudflare bật Node.js compatibility mặc định cho Worker mới, nâng giới hạn kích thước lên 64 mebibyte, và xây lại module registry dựa trên URL thay vì cơ chế resolve phụ thuộc bundler cũ. Mình có sẵn một API nội bộ nhỏ đang nằm đúng lằn ranh đó, nên dành một buổi chiều thực sự migrate thay vì chỉ đọc thông báo rồi gật đầu.
Cái mình migrate
Một service nội bộ ~40 route: routing kiểu Express, vài npm dependency để verify JWT và xử lý ngày tháng, đọc từ Postgres qua pg. Không có gì lạ — mà đó chính xác là kiểu workload trước đây hay đâm vào tường trên Workers, vì native binding của pg và các giả định về module net của Node chưa bao giờ hoạt động trọn vẹn dưới bộ polyfill nodejs_compat cũ.
// wrangler.jsonc — toàn bộ thay đổi về compat giờ chỉ có vậy
{
"name": "internal-api",
"main": "src/index.ts",
"compatibility_date": "2026-09-09",
"compatibility_flags": ["nodejs_compat"]
}
Trước ngày 9/9, flag đó cho bạn Buffer, EventEmitter, và một phần crypto — đủ cho nhiều thư viện, chưa đủ cho bất cứ thứ gì cần raw TCP. pg cần raw TCP. Trước đây nó fail ngay lập tức với lỗi khó hiểu Cannot resolve module "net", khiến nhiều người lạc vào mê cung tìm driver Postgres thay thế dựa trên HTTP. Giờ nó resolve và kết nối được. Riêng thay đổi đó là khoảng cách giữa “phải viết lại data layer để chạy Workers” và “deploy thẳng data layer hiện có lên Workers”.
Giới hạn 64 MiB thực sự quan trọng ở đâu
Giới hạn 10 MB nén cũ không thực sự nằm ở code của bạn — nó nằm ở node_modules. Bất cứ thứ gì kéo theo một cây dependency tương đối phức tạp (một ORM, thư viện PDF, bộ xử lý ảnh) đều vượt giới hạn trước khi bạn viết xong route đầu tiên. Bundle của service này sau khi qua esbuild với tree-shaking, tính cả dependency, ra khoảng 18 MB. Với giới hạn cũ đó là fail cứng. Với 64 MiB nó deploy thoải mái còn dư chỗ.
Con số này ít quan trọng hơn thông điệp nó gửi đi: Cloudflare đang nói thẳng “đừng pre-optimize kích thước bundle trước khi bạn còn chưa ship được một bản chạy được.” Mình từng tốn không ít giờ công thật của client để tách thủ công worker bundle hoặc tự viết lazy import chỉ để lách dưới 10 MB. Giờ đó là công sức lãng phí với phần lớn service, không phải vì Workers chạy nhanh hơn, mà vì cái ràng buộc từng biện minh cho việc đó đã biến mất.
Phần thay đổi module registry — chẳng ai nói tới
Tính năng nổi bật là Node.js compat. Nhưng thay đổi mình thực sự nhận ra khi migrate là cơ chế resolve module dựa trên URL. Workers giờ resolve module theo cách trình duyệt làm — theo URL, với lazy compilation, thay vì bắt buộc mọi thứ phải được pre-bundle thành một khối phẳng trước khi deploy. Kết hợp với shared code cache giữa các isolate, cold start của service mình giảm từ khoảng 40ms xuống dưới 15ms khi cache đã ấm, vì Cloudflare không còn phải compile lại cùng một dependency graph cho mỗi isolate mới sinh ra.
// import.meta giờ hoạt động giống hệt trong Node/browser,
// không còn là một stub riêng của Workers
if (import.meta.url === new URL(process.argv[1], "file://").href) {
runMigrations();
}
Bản thân nó là chuyện nhỏ, nhưng đúng kiểu chuyện nhỏ trước đây buộc bạn phải rải rác typeof importMeta !== "undefined" khắp code phải chạy được cả trên Node lẫn Workers. Bỏ được sự khác biệt đó là bỏ luôn cả một nhóm bug kiểu “chạy local ổn, deploy lên là hỏng”.
Cái vẫn chưa chạy được
Không phải mọi thứ đều miễn phí. Hai dependency của mình dùng fs để đọc file config local lúc khởi động — một pattern vô nghĩa trên Workers dù bật compat flag nào đi nữa, vì edge không có filesystem để đọc. Đây không phải lỗ hổng của Cloudflare; nó là lời nhắc rằng “tương thích Node.js” nghĩa là tương thích API, không phải tương thích mô hình triển khai. Mình đổi các chỗ đọc đó sang dùng env binding — đó là cách sửa đúng trên bất kỳ nền tảng serverless nào, không phải một workaround riêng của Workers.
Kết luận thực sự cho một quyết định migrate
Nếu bạn từng từ chối Workers cho một service Node hiện có trong một hai năm qua vì lỗ hổng của nodejs_compat, quyết định đó đã lỗi thời — quay lại test thử đi, vì kiểu lỗi bạn từng gặp trước đây (raw TCP, native module, giả định có filesystem) giờ là một tập hợp nhỏ hơn hẳn. Nếu service của bạn thực sự cần filesystem thật hoặc ngữ nghĩa TCP server sống lâu dài vượt ngoài kết nối outbound, Workers vẫn không phải đích đến đúng, và không flag compat nào đổi được điều đó. Câu hỏi hữu ích không phải “Node.js compat giờ đã tốt chưa” — mà là “API cụ thể nào service của mình phụ thuộc từng bị thiếu, và nó có nằm trong danh sách vừa được thêm vào không.” Đó là một bài test một buổi chiều, và đáng để chạy trước khi bạn gạch bỏ nền tảng này lần nữa.