Khi endpoint Flask phải trả về dữ liệu lớn — ví dụ file CSV vài trăm nghìn dòng — đừng gom hết vào một chuỗi rồi mới return: server giữ toàn bộ dữ liệu trong RAM, client chờ tới khi xong mới nhận được byte đầu tiên. Cách đúng là stream từng phần bằng generator kết hợp stream_with_context: Flask gửi từng chunk ngay khi có, RAM chỉ giữ một phần nhỏ, và request context vẫn sống suốt quá trình stream.
Vấn đề
Giả sử bạn viết endpoint xuất CSV đơn hàng. Nhiều người gom mọi dòng vào một list rồi nối thành một chuỗi lớn trước khi trả response. Với vài nghìn dòng thì không sao, nhưng với vài trăm nghìn dòng, RAM của server phình lên theo đúng kích thước file, còn client chờ trắng cả phút mới thấy byte đầu tiên — và nếu request timeout giữa chừng, toàn bộ công sức đó đổ sông đổ bể.
Cái bẫy thứ hai tinh vi hơn: bạn đổi sang generator cho nhẹ RAM, nhưng generator lại đọc dữ liệu từ database qua g hoặc session. Flask pop request context ngay khi hàm view return, nên lúc generator bắt đầu chạy, g đã biến mất và bạn nhận lỗi RuntimeError: Working outside of application context — lỗi quen thuộc với ai lần đầu stream trong Flask.
from flask import Flask, Responseapp = Flask(__name__)@app.route("/export")def export(): rows = ["id,total"] for order in Order.query.all(): # tải hết vào RAM rows.append(f"{order.id},{order.total}") return Response("\n".join(rows), mimetype="text/csv")
Giải pháp
Bọc generator bằng stream_with_context rồi trả về qua Response. Flask giữ request context sống suốt quá trình stream, nên generator vẫn dùng được g, request và session như trong view bình thường, đồng thời gửi từng chunk cho client ngay khi nó được tạo ra. Kết hợp yield_per() của SQLAlchemy để đọc dữ liệu theo từng đợt nhỏ.
from flask import Flask, Response, stream_with_contextapp = Flask(__name__)@app.route("/export")def export(): def generate(): yield "id,total\n" for order in Order.query.yield_per(1000): # đọc từng đợt 1000 dòng yield f"{order.id},{order.total}\n" return Response( stream_with_context(generate()), mimetype="text/csv", headers={"Content-Disposition": "attachment; filename=orders.csv"}, )
Vì Flask không biết trước tổng độ dài nên response dùng chunked transfer encoding, trình duyệt bắt đầu tải file ngay từ chunk đầu tiên. Lưu ý: đừng tự đặt header Content-Length khi stream, vì bạn không biết trước độ dài chính xác.
Cách này không chỉ dùng cho CSV. Bạn có thể stream JSON Lines với mimetype="application/x-ndjson" để client xử lý từng dòng ngay khi nhận, hoặc stream log theo thời gian thực cho trang theo dõi tiến trình.
Áp dụng vào dự án của bạn
Rà soát các endpoint xuất file, xuất báo cáo hoặc trả danh sách dài: chỗ nào đang gom toàn bộ kết quả vào list rồi mới return thì đổi sang generator bọc stream_with_context. Bắt đầu từ endpoint nặng nhất — thường là xuất CSV hoặc báo cáo — và đo lại RAM đỉnh trước và sau khi đổi. Nhớ quy tắc vàng: generator nào chạm tới g, request hay database thì luôn phải đi qua stream_with_context, nếu không lỗi RuntimeError sẽ chờ bạn ở production.
Bài viết liên quan
- Flask: trả file tải về an toàn bằng send_file trong 5 phút
- Flask: dùng Application Factory (create_app) để app dễ test, dễ mở rộng ngay từ đầu
- Flask: ghi log mọi request tự động bằng before_request và after_request trong 5 phút
Câu hỏi thường gặp
Không dùng stream_with_context thì sao?
Generator vẫn chạy, nhưng request context đã bị pop sau khi view return. Chỉ cần generator chạm tới g, request, session hay database là bạn nhận ngay RuntimeError — Working outside of application context với g/session, Working outside of request context với request. Generator chỉ yield chuỗi cố định thì không bọc cũng chạy, nhưng bọc luôn cũng không mất gì.
Stream có làm response chậm hơn không?
Tổng thời gian gần như không đổi, nhưng client nhận byte đầu tiên sớm hơn nhiều, vì Flask gửi chunk ngay khi có thay vì chờ gom đủ. Với file lớn, trải nghiệm người dùng tốt hơn rõ rệt và nguy cơ timeout giữa chừng cũng giảm.
Có dùng được stream_with_context cho JSON không?
Được. Yield từng dòng JSON theo định dạng JSON Lines, đặt mimetype="application/x-ndjson", client xử lý từng dòng ngay khi nhận. Đừng stream một JSON object lớn bằng cách yield từng mảnh của nó, vì client phải đợi đủ mảnh mới parse được — mất hết lợi ích của streaming.
💬 Bạn đã thử cách này trong dự án của mình chưa? Nếu gặp lỗi hoặc có cách làm gọn hơn, để lại bình luận bên dưới — mình đọc hết và sẽ bổ sung những ý hay vào bài.
Nguồn tham khảo
- Flask Documentation — Streaming: https://flask.palletsprojects.com/en/stable/patterns/streaming/
- Flask API — stream_with_context: https://flask.palletsprojects.com/en/stable/api/#flask.stream_with_context
