Tôi đã ép xung API từ 200ms xuống còn 10ms như thế nào?
Này các đồng chí, khi API chậm như sên, độ trễ P95 cao chót vót, và server lăn ra chết lúc 3 giờ sáng vì traffic đột biến, các bạn sẽ chọn cách nào:
- Bỏ ra 3 tháng viết lại mọi thứ bằng Rust.
- Ngồi nhìn người dùng bỏ đi.
Hoặc, các bạn có thể "ăn gian" giống như tôi.

Tôi đã ghép tốc độ cực hạn của Bun vào hệ sinh thái khổng lồ của Node.js. Đừng cười, tôi nghiêm túc đấy. Tôi đã nén một backend cồng kềnh xuống dưới 10ms mà không cần viết lại 5 năm logic nghiệp vụ cũ kỹ (legacy).
1. Bun ở tiền tuyến, Node.js làm công nhân hậu phương
Ai cũng biết Node xử lý HTTP request có overhead (chi phí phụ) khá lớn. Nhưng logic nghiệp vụ của tôi lại toàn phụ thuộc vào các thư viện crypto và SDK cũ rích, không thể nào port sang Bun được.
Giải pháp của tôi là mô hình "Tiền trạm - Hậu phương".
Tôi dùng Bun để dựng một lớp HTTP siêu mỏng, chỉ lo việc định tuyến (routing), validate tham số và chặn các request rác. Chỉ khi nào cần xử lý logic nghiệp vụ cũ, tôi mới ném task đó cho một tiến trình Node thường trú thông qua IPC (Inter-Process Communication).
Lưu ý sống còn: Đừng bao giờ spawn một tiến trình Node khi có request tới. Làm thế còn chậm hơn dùng Node đơn thuần. Bạn phải khởi động trước một nhóm Node Worker và giữ cho chúng luôn "nóng" (warm up).
Phía Bun (Gateway):
// bun-gateway.ts
const textDecoder = new TextDecoder();
const textEncoder = new TextEncoder();
// Khởi động một tiến trình Node thường trú, không phải mỗi request 1 process
const nodeWorker = Bun.spawn(["node", "heavy-lifter.js"], {
stdin: "pipe",
stdout: "pipe",
});
// Hàm wrap đơn giản để ném việc bẩn cho Node
async function askNode(payload: any) {
const msg = JSON.stringify(payload) + "\n";
nodeWorker.stdin.write(textEncoder.encode(msg));
// Logic đọc đơn giản hóa (Lên production nhớ xử lý dính gói - sticky packets!)
const reader = nodeWorker.stdout.getReader();
const { value } = await reader.read();
return JSON.parse(textDecoder.decode(value));
}
Bun.serve({
port: 3000,
async fetch(req) {
if (req.url.endsWith("/fast")) return new Response("Bun is fast!");
// Chỉ những việc nặng mới nhờ đến Node
if (req.url.endsWith("/heavy")) {
const data = await req.json();
const result = await askNode(data);
return Response.json(result);
}
return new Response("404", { status: 404 });
},
});
Phía Node (Worker):
// heavy-lifter.js
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false
});
rl.on('line', (line) => {
const data = JSON.parse(line);
// Giả vờ đang tính toán mã hóa rất nặng
// Code Node cũ chạy ở đây, không cần sửa gì cả
const result = { processed: true, echo: data };
console.log(JSON.stringify(result));
});
Với cách này, routing và I/O đạt tốc độ dưới mili giây, trong khi Node chỉ tập trung vào tính toán thuần túy. Hiệu suất tăng gấp đôi ngay lập tức.
2. Đừng bắt CPU đi bốc vác: Zero-Copy với Bun
Tôi phát hiện CPU server cao bất thường chỉ vì chúng tôi đang đọc file config và JSON tĩnh từ ổ cứng, serialize lại rồi gửi cho user.
Trong Node, bạn thường dùng fs.readFile rồi res.send. Việc này gây ra nhiều lần copy dữ liệu: Ổ cứng -> Kernel -> User Space Buffer -> Socket.
Trong Bun, tôi đổi sang dùng Bun.file(). Đây không chỉ là thay đổi cú pháp, nó bảo hệ điều hành: "Ném thẳng file này ra card mạng đi, đừng qua tay tôi."
// Đừng readFile nữa, stream trực tiếp luôn
Bun.serve({
fetch(req) {
if (req.url.endsWith("/config")) {
return new Response(Bun.file("./big-config.json"));
}
return new Response("404");
}
});
Một dòng code này giúp thông lượng tài nguyên tĩnh của tôi tăng gấp 3 lần.
3. Micro-batching: Xếp hàng đi!
Thứ đáng sợ nhất của cao tải (high concurrency) là gì? Là 1000 request ập vào cùng lúc, mỗi cái lại gọi database hoặc gọi Node process một lần riêng biệt. Giống như sinh viên ùa xuống căng tin giờ tan học vậy.
Tôi thêm vào một cửa sổ đệm (buffer window) cực nhỏ. Nếu trong vòng 3ms có 50 request tới, tôi gói chúng thành một mảng và gửi cho Node hoặc DB một lần duy nhất.
let buffer: any[] = [];
let timer: Timer | null = null;
function processBatch() {
const currentBatch = buffer;
buffer = [];
timer = null;
// Gửi 50 task cho Node một lần, thay vì 50 lần
askNode({ type: 'batch', items: currentBatch });
}
function enqueue(item: any) {
buffer.push(item);
// Chỉ bắt đầu đếm giờ khi có item đầu tiên vào hàng
if (!timer) {
timer = setTimeout(processBatch, 3); // 3ms user không cảm nhận được, nhưng thông lượng tăng khổng lồ
}
}
Chờ 3ms đổi lại việc giảm 60% tải CPU.
4. Làm ơn đừng new Object trong vòng lặp
Khi review code, tôi thấy nhiều người hay viết const db = new DatabaseClient() hoặc const regex = new RegExp(...) ngay trong hàm fetch hoặc handleRequest.
Cấp phát lại bộ nhớ, thiết lập kết nối, biên dịch regex cho mỗi request là công thức hoàn hảo để làm nổ tung bộ dọn rác (Garbage Collection).
Hãy đưa tất cả những thứ tái sử dụng được — DB pool, TextEncoder, RegEx, Key mã hóa — ra phạm vi toàn cục (global scope). Trong kiến trúc lai Bun/Node, điều này cực quan trọng vì chúng ta đang đua tốc độ từng micro giây.
5. Cache 2 lớp: Khi RAM là không đủ
Trước đây tôi chỉ dùng Redis, nhưng request qua mạng vẫn có độ trễ. Sau đó tôi nhận ra Bun đọc file siêu nhanh.
Thế là tôi dựng Cache 2 lớp:
- L1 Memory Cache: Dùng LRU lưu 1000 key nóng nhất. Phản hồi tính bằng micro giây.
- L2 File Cache: Ghi dữ liệu ít nóng hơn ra file JSON vào
/tmp/cache/.
Kiểm tra file tồn tại nhanh hơn nhiều so với việc mở kết nối TCP tới Redis.
6. Vứt bỏ các gói npm phình to
Trong Node, chúng ta hay quen tay npm install uuid hay qs chỉ để tạo UUID hoặc parse tham số.
Trong Bun (và Node hiện đại), crypto.randomUUID() và URLSearchParams đã được tích hợp sẵn và tối ưu ở tầng C++.
Tôi gỡ bỏ tất cả dependency npm không cần thiết và chuyển sang dùng Native API. Việc này không chỉ giúp khởi động nhanh hơn (cold start) mà quan trọng hơn là giảm bớt cơn ác mộng I/O của node_modules.
7. Giải quyết môi trường phát triển "đa nhân cách"
Kiến trúc này dùng Bun làm Gateway và Node làm Compute. Nhưng ở local, tôi suýt phát điên.
Laptop tôi chạy Node 22. Để bảo trì dự án cũ, tôi cần Node 14. Tôi cũng cần Bun, và thỉnh thoảng là Deno để chạy script. Chuyển đổi qua lại bằng nvm thực sự mệt mỏi — xung đột cổng, lỗi đường dẫn, biến môi trường loạn cào cào. Sửa được môi trường Bun thì Node cũ lại chết.
Sau đó tôi tìm thấy ServBay. Nó là cứu cánh cho developer. Không phải là cái tool chuyển version sơ sài, nó là một nền tảng môi trường runtime cách ly hoàn chỉnh.
- Cùng tồn tại đa phiên bản: Tôi có thể chạy môi trường Node 14, Node 22 và Bun 1.1 cùng lúc. Chúng hoàn toàn cách ly và không đánh nhau.
- Hệ sinh thái một chạm: Tôi có thể cài đặt cơ sở dữ liệu chỉ với một cú click (Redis làm cache, PostgreSQL lưu dữ liệu), thậm chí cả Caddy làm reverse proxy. Mọi thứ cứ thế mà chạy.
- Zero Config: Tôi chợt nhận ra mình đã lãng phí bao nhiêu thời gian để cấu hình Docker và Homebrew trước đây.

Với ServBay, tôi tái lập hoàn hảo kiến trúc lai production ngay tại local: Bun lắng nghe cổng 3000, Node lắng nghe pipe nội bộ, và Redis chạy ngầm. Tôi không còn phải lo lắng xem lỗi do code hay do môi trường nữa.
Lời kết
Miễn là ép được thời gian phản hồi xuống mức 10ms, tôi không quan tâm mình phải trộn bao nhiêu loại runtime.
Bun cho tôi tốc độ. Node cho tôi sự ổn định. ServBay cho tôi một môi trường không gây ức chế.
Đừng đắn đo xem nên dùng Bun hay Node.js nữa. Người lớn cả rồi, sao không chọn cả hai? Kết hợp chúng lại, và đi cắt giảm 90% độ trễ API của bạn ngay đi.
All rights reserved