最近在使用流式输出进行开发,总结出来以下几个经验
整体主要是关于以下这几点:
- 客户端断开 ≠ 服务端自动停止
- 必须感知 client disconnect
- 阻塞 / CPU 任务不能直接放在流里
- 生成器要能被安全 cancel
- 异常不能直接 raise
- 连接数 ≠ 并发任务数
- SSE 一定要禁用代理缓存
以下是具体会碰到的问题:

1. 客户端关闭,服务端还在跑
此处是前端用户已经关闭了,但是后端仍然还给前端在发送消息,出现这个问题的最主要原因是没有检测client disconnect,FastAPI的流式请求用户断开后不会自动给断开,需要通过FastAPI包中的Request包中的is_disconnected方法来进行监测,此处是使用异步的方式来进行监测
import asycio
from fastapi import Request
@app.get("/sse")
async def sse(request: Request):
async def event_generator():
try:
while True:
# 判断request是否还在连接 检测时需要再前面添加await
if await request.is_disconnected():
print("client disconnected")
break
yield "data: processing\n\n"
await asyncio.sleep(1)
except asycio.CancelledError:
# 生成器取消后安全关闭其他资源操作
error_response = {"message": "任务被取消"}
yield f"data: {error_response}\n\n"
return StreamingResponse(
event_generator(request),
media_type="text/event-stream"
)
2. 后台任务不要放在流式响应中执行
不要将耗时的后台任务放在流式响应中进行执行,流式响应只适合做实时响应,这样会导致任务执行一直卡死在SSE并且无返回,并且在执行流式响应时需要设置抖动(尤其针对于轮询/进度条的流式响应),防止一直在执行流式响应导致系统卡死
针对于后台任务可以采用放到celery的队列中,队列一般采用redis,如果是比较简单的任务可以采用anyio/asyncio(内存执行)的方式,然后再通过轮询SSE的方式来检查任务执行状态
3. SSE中不要抛出异常
在SSE中不要抛出异常,抛出异常会导致SSE连接直接终端,即使是错误也要通过流式的方式返回后然后再手动停止并回收相关资源,否则前端直接终端后也无法显示错误信息
try:
...
except Exception as e:
# 将错误信息可以返回给前端
yield f"event: error\ndata: {str(e)}\n\n"
4. SSE Header一定要设计完整
需要设置好准确的SSE Header,否则会导致:
- nginx缓冲
- 前端等待很久才收到消息
|-----------|---------------------------|
| 问题 | 对应 Header |
| 数据被缓存 | Cache-Control: no-cache |
| 连接被中间层断掉 | Connection: keep-alive |
| 数据被反向代理缓冲 | X-Accel-Buffering: no |
# SSE的header模板
headers = {
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no", # nginx
}
Cache-Control: no-cache :是为了防止HTTP缓存,但是如果HTTP进行缓存后浏览器直接使用旧响应,也就是再缓存过期之前新传回的SSE数据不再生效,这个部分的设置是为了告诉前端这个响应是实时的,禁止缓存,必须实时传输
Connection: keep-alive :是告诉middleware这个连接时长连接
X-Accel-Buffering: no :这个时告诉nginx不要进行缓存,每一段都需要立刻转发
5. SSE的返回格式
最好是将SSE的返回格式封装成一个函数,参考如下:
@dataclass
class _ResponseBody:
"""
统一响应体结构
"""
code: int = 0
message: str = ""
data: Any = None
language: str | None = None
def to_dict(self) -> dict:
"""转换为字典,保留 None 值"""
return {
"code": self.code,
"errorMsg": self.message,
"data": self.data,
"language": self.language
}
def sse_response(*, data: Any = None, code: int = 0, language: str = None, message: str = "") -> str:
"""
将内容包装为 SSE (Server-Sent Events) 响应
"""
body = _ResponseBody(code=code, message=message, data=data, language=language)
return wrap_sse_data(body.to_dict())
def wrap_sse_data(content: str | dict | list | int | float | bool | None) -> str:
"""
将内容包装为 SSE (Server-Sent Events) 格式
支持 str, dict, list 及其他 JSON 可序列化类型
"""
if not isinstance(content, str):
# 序列化任何非字符串类型并确保是 utf-8 字符串
content = orjson_dumps_bytes(content).decode("utf-8")
# SSE返回格式
return f"data: {content}\n\n"
def orjson_dumps_bytes(obj: Any, *, default: Any = None, option: int | None = None) -> bytes:
"""
高性能 JSON 序列化(直接返回 bytes)。
最佳场景:
- HTTP Response (Starlette/FastAPI)
- 写入 Redis/消息队列
- 存入文件
"""
handler = default if default is not None else _enhanced_default_handler
final_option = DEFAULT_ORJSON_OPTIONS | option if option is not None else DEFAULT_ORJSON_OPTIONS
try:
return orjson.dumps(obj, default=handler, option=final_option)
except Exception as e:
# 包装异常,提供更清晰的上下文
raise ValueError(f"JSON Serialization Failed: {str(e)} - Type: {type(obj)}") from e