Vấn đề: gom hết dữ liệu vào bộ nhớ rồi trả trong một HTTP request đồng bộ → tốn RAM, vượt timeout của proxy/gateway. Hai hướng:
1. Streaming response (dữ liệu vừa/lớn vừa):
- Query theo lô (cursor/LIMIT), ghi từng chunk ra response stream, Transfer-Encoding: chunked, không giữ toàn bộ trong RAM.
- Đơn giản, tải về ngay, nhưng vẫn giữ một kết nối mở lâu — dễ đứt nếu quá dài, và bên trước (CDN/LB) vẫn có thể timeout.
2. Background job (khuyến nghị cho dữ liệu rất lớn):
- Request chỉ tạo job (đẩy vào queue) và trả 202 Accepted + job_id ngay.
- Worker chạy nền: query theo lô, ghi file, upload lên object storage (S3).
- Khi xong, cho user tải qua presigned URL (link có hạn, không lộ credential) — báo qua polling GET /jobs/{id}, email, hoặc notification realtime.
// streaming: batch by cursor, flush each chunk, never buffer all rows
res.setHeader("Content-Type", "text/csv")
res.setHeader("Transfer-Encoding", "chunked")
for await (const row of db.cursor("SELECT * FROM orders WHERE ...")) {
res.write(toCsvLine(row))
}
res.end()
// very large / heavy: return 202 + job_id instead; a worker uploads to S3 (presigned URL)Chọn: vài nghìn dòng → streaming đủ. Hàng trăm nghìn+/ báo cáo nặng → background job, thoát hẳn khỏi vòng đời request.
Lỗi hay gặp: load toàn bộ vào mảng trong RAM; giữ transaction mở suốt export; để link tải công khai vĩnh viễn thay vì presigned URL hết hạn.