正确做法是用生成器惰性分块输出,如def gen(): yield '{"id":1}'; yield '{"id":2}',再传给streamingresponse或response;禁用nginx缓存、避免sleep/print、捕获brokenpipeerror、不用json.dumps全量序列化。

用 response.stream 做分块输出,别用 json.dumps 一次性塞
大文件响应卡死、内存爆掉,八成是因为你把整个文件读进内存再返回。Python Web 框架(如 Flask、FastAPI)里,response.stream 是唯一靠谱的出口——它不攒数据,边生成边发。但注意:stream=True 只是开关,真正起作用的是你传给它的可迭代对象必须「惰性」。
- 别写
return json.dumps(huge_list),哪怕加了stream=True也没用,dumps本身就会全量序列化 - 正确做法是用生成器:比如
def gen(): yield '{"id":1}'; yield '{"id":2}',然后return Response(gen(), mimetype="application/json") - FastAPI 的
StreamingResponse和 Flask 的Response都认生成器,但 Flask 默认不设Content-Length,客户端可能等不到 EOF
分块时别在循环里调 time.sleep 或 print
有人想“模拟流式”,在生成器里每 yield 一次就 time.sleep(0.1),或者打个 print 调试——这会阻塞响应管道,尤其部署在 Gunicorn/Uvicorn 时,worker 线程被占着,后续请求全堵住。
-
print在非调试模式下通常被重定向到日志,但依然有 IO 开销;生产环境直接删掉 -
time.sleep是同步阻塞,除非你明确要限速(比如导出限流),否则毫无必要 - 真要观察流式效果,用 curl 看原始字节:
curl -N http://localhost:8000/export,-N关闭缓冲
Content-Type 和 Transfer-Encoding: chunked 别手动设
手动设 Content-Type 容易错,更危险的是手写 Transfer-Encoding: chunked。现代 HTTP 服务器(Nginx、Uvicorn)和客户端都依赖这个头做流判断,但它是底层传输层自动加的,不该由业务代码碰。
- 只设
Content-Type,比如text/csv; charset=utf-8或application/x-ndjson,其他交给框架 - 如果用了 Nginx 反代,默认会缓存响应体,导致流失效——必须加
proxy_buffering off;和chunked_transfer_encoding on; - 浏览器 fetch API 接收流式响应时,
response.body.getReader()才能按块读,response.json()会等到结束,别混用
大文件导出失败?先查 ConnectionResetError 和 BrokenPipeError
这两个错误不是你的代码崩了,而是客户端(浏览器关页、curl 中断、移动端切后台)提前断开连接,服务端还在往 socket 写,就抛 BrokenPipeError。不处理它,整个 worker 可能卡住或报 500。
- Flask 里可以在生成器里 try/except
GeneratorExit或BrokenPipeError,然后主动 return - FastAPI 更推荐用
BackgroundTasks把导出变成异步任务,返回下载链接,彻底避开长连接问题 - 数据库游标别复用:分块查库时,每次
fetchmany(n)后立刻 yield,别等全查完再 yield,否则 PG 的server_side_cursor也可能被超时干掉










