Non-blocking I/O là mô hình mà Node.js gửi thao tác I/O (đọc ghi file, query DB, gọi HTTP…) xuống kernel hoặc thread pool rồi trả quyền điều khiển về event loop ngay lập tức, không chờ kết quả. Khi I/O xong, callback được đẩy vào queue để event loop gọi lại.
Hình dung cho dễ: giống nhà hàng một đầu bếp (event loop) — khách gọi món, đầu bếp chuyển order xuống bếp (OS kernel), rồi nhận khách tiếp. Món xong, bưng ra bàn. Đầu bếp không đứng chờ mỗi bàn mà luân chuyển liên tục.
Cơ chế cụ thể: khi bạn gọi fs.readFile(path, callback), Node.js đăng ký thao tác với libuv. libuv quyết định gửi xuống OS kernel (nếu OS hỗ trợ async, ví dụ epoll trên Linux) hoặc dùng internal thread pool (mặc định 4 thread, chỉnh được qua UV_THREADPOOL_SIZE). Dù theo đường nào, event loop đều không bị chặn.
So với mô hình thread-per-request: Apache cấp một thread cho mỗi request — thread đó nằm chờ khi query DB. 1000 request đồng thời cần 1000 thread, tốn khoảng 1GB RAM. Node.js xử lý 1000 request trên một thread, vì phần lớn thời gian chờ I/O là thời gian rảnh (idle). Số liệu thực tế: Node.js thường đạt 10k–50k req/s trên API nặng I/O, Apache khoảng 1k–5k req/s.
Lỗi thường gặp — vô tình chặn event loop:
- fs.readFileSync() — block cho đến khi file đọc xong
- JSON.parse() trên payload lớn — CPU-bound, giữ event loop
- Vòng lặp tính toán nặng trong request handler
- crypto.pbkdf2Sync() — synchronous crypto
Tất cả đều khiến mọi request khác xếp hàng chờ.
Cách tránh: luôn dùng biến thể async (fs.readFile thay vì fs.readFileSync), và đẩy phần CPU-intensive sang Worker Threads.
Lưu ý: non-blocking I/O giúp Node.js hiệu quả với I/O-bound workload, nhưng không phải giải pháp cho mọi bài toán. Nếu phần lớn thời gian là tính toán CPU (image processing, ML inference), Worker Threads hoặc service tách riêng mới là hướng đúng.