fastapi中uploadfile默认将整个文件加载到内存,易引发oom;应通过file.file直接访问底层对象,结合线程池+分块读取实现安全流式处理,并注意asgi服务器与反向代理的缓冲配置。

FastAPI 中 UploadFile 默认行为会把整个文件读进内存
哪怕你只调用 await file.read() 一次,FastAPI 也会在内部把全部内容加载到内存里。对几百 MB 的日志、视频或 CSV 文件,这直接触发 MemoryError 或让服务 OOM 被杀掉。这不是你代码写错了,是默认流式支持被绕过了。
关键点在于:UploadFile 的 file 属性是个 SpooledTemporaryFile,它在小文件时驻留内存,大文件才落盘——但“大”的阈值(max_memory=1024*1024*5,即 5MB)不可控,且一旦超过,磁盘 I/O + 内存缓冲仍可能卡住异步协程。
- 不要用
await file.read()或await file.spool(),它们都试图一次性消费全部内容 - 改用
file.file直接访问底层文件对象(注意:它可能是io.BytesIO或tempfile.SpooledTemporaryFile) - 对大文件,优先判断是否已落盘:
hasattr(file.file, 'name')为True才能安全用open(file.file.name, 'rb')
用 iter_chunks() 实现可控流式读取
FastAPI 本身没提供流式迭代器,但你可以手动封装一个基于 file.file.read() 的分块读取逻辑,配合 yield 让协程不阻塞。
示例函数:
async def stream_upload_file(file: UploadFile, chunk_size: int = 8192):
while True:
chunk = await file.read(chunk_size)
if not chunk:
break
yield chunk
这个写法看似简单,但要注意:file.read() 在底层仍是同步阻塞调用,如果文件已落盘且体积极大,它会卡住事件循环。更稳妥的方式是把读取操作扔进线程池:
- 用
loop.run_in_executor(None, file.file.read, chunk_size)替代await file.read() - 确保
file.file支持.read()(BytesIO和SpooledTemporaryFile都支持) - 别设过小的
chunk_size(如 1024),否则系统调用开销反超收益
对接 StreamingResponse 时避免中间缓存
如果你是上传后立刻转成响应流(比如做代理、格式转换或校验),千万别先写到临时文件再读出来——这等于复制两遍。
正确链路是:上传流 → 处理流(如解压、加字段、过滤行)→ 响应流,全程不落地。
- 处理逻辑必须是异步生成器,例如
async def process_chunk(chunk: bytes) -> bytes: - 传给
StreamingResponse的可迭代对象,类型必须是AsyncGenerator[bytes, None],不能是普通生成器或列表 - 若需在流中插入 HTTP header(如
Content-Disposition),得提前计算好总大小;否则只能用Transfer-Encoding: chunked,浏览器可能无法显示进度条
真实场景下容易漏掉的边界问题
流式不是万能解药,几个硬伤必须手动兜底:
- 客户端中断上传时,
UploadFile不会自动抛异常,需在每次await file.read()后检查request.is_disconnected() - 多个并发大文件上传可能耗尽线程池,默认
ThreadPoolExecutor最大线程数是 CPU 核心数 × 5,建议显式配置max_workers=10 - Windows 下
SpooledTemporaryFile落盘后,file.file.name指向的是临时目录,路径权限或防病毒软件可能拦截读取 - 用
StreamingResponse返回时,Nginx 默认proxy_buffering on,会攒满 4KB 才透传,需加配置proxy_buffering off;或调大proxy_buffers
流式读取真正起效的前提,是你清楚每一层(FastAPI、ASGI server、反向代理、客户端)各自怎么处理字节流——少一个环节缓冲,就可能前功尽弃。
大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!











