OpenAI Realtime API WebSocket 断连 4008/1006 怎么解决不是 Key 失效是实时多模态独有的会话超时规则上周三晚上我在做一个语音助手 demo用 OpenAI Realtime API 跑多模态对话。跑了大概 3 分钟突然断了控制台里赫然一个close code 4008。我第一反应是 Key 挂了——但同一个 Key 拿去调 gpt-5.5 的 REST 接口完全正常。折腾到凌晨两点才搞明白OpenAI Realtime API 的 WebSocket 有两种独有的断连机制跟普通 Chat Completions API 完全不是一回事。直接给结论close code 4008 是session.expires_at到期未续期据实测推断对应 session expiredclose code 1006 是音频 chunk 发送间隔超过服务端阈值实测约 25 秒无数据官方未公布精确值导致异常关闭。两者的解法都是客户端主动发心跳/续期指令下面给完整代码。为什么会出现这两种断连先理清 OpenAI Realtime API 跟普通 REST 接口的区别sequenceDiagram participant C as 客户端 participant W as Realtime API WebSocket participant S as OpenAI 服务端 C-W: 建立 WebSocket 连接 W-S: session.create S--C: session.created (含 expires_at) Note over C,S: 会话倒计时开始默认最长 30 分钟 C-S: audio chunk (每100-200ms) S--C: audio/text response Note over C,S: 若实测约25s无任何数据帧→1006 Note over C,S: 若expires_at到期未续→4008普通的/v1/chat/completions是请求-响应模型一次请求一次回复没有会话过期这个概念。但 OpenAI Realtime API 是持久 WebSocket 连接服务端会主动踢人。close code 4008session expiredOpenAI 给每个 Realtime session 设了一个expires_at时间戳官方文档记载默认最大会话时长为 30 分钟实际观测到的过期时间可能因实现版本而异如有差异请以官方最新文档为准。到期前你需要发送session.update续期否则服务端直接关连接。注4008 对应 session expired 为实测推断OpenAI 官方文档未直接确认此映射关系。session.update能否重置expires_at同样来自社区帖子而非官方文档存在不确定性详见下文方案一说明。close code 1006abnormal closure这个更隐蔽。WebSocket 协议里 1006 表示连接异常关闭没有收到 close frame。在 OpenAI Realtime API 场景下如果你的音频流中断超过服务端阈值实测约 25 秒官方未公布精确值比如用户沉默、麦克风暂停、代码里 buffer 卡住了服务端认为连接已死直接断开。客户端收到的就是 1006没有任何 close frame。方案一处理 4008——在 expires_at 前主动续期服务端在session.created事件里会返回expires_at字段Unix 时间戳。你需要在过期前 60 秒左右发一个续期消息60 秒为经验值非官方规定。注意session.update需要携带session字段否则服务端可能报错或忽略import json, time, threading def schedule_renewal(ws, expires_at): delay expires_at - time.time() - 60 if delay 0: time.sleep(delay) ws.send(json.dumps({ type: session.update, session: {} # 至少传空对象实际可填需更新的字段 }))⚠️ 重要说明session.update的官方用途是更新会话配置如 modalities、voice、instructions 等。发送session.update能否重置expires_at并未在官方文档中明确说明此行为来自社区帖子待官方确认。若该行为不成立建议改用重建连接作为备选方案即在expires_at前主动断开并重新建立 WebSocket 连接。收到session.updated响应后拿到新的expires_at再递归调度下一次续期。如果你通过 ofox.io 或 OpenRouter 这类聚合网关接入 Realtime APIsession.created事件结构与直连一致expires_at字段同样可用以下续期逻辑无需修改def on_message(ws, msg): data json.loads(msg) if data[type] session.updated: new_exp data[session][expires_at] threading.Thread(targetschedule_renewal, args(ws, new_exp)).start()库依赖说明以上代码假设ws为websocket-client库的WebSocketApp实例。在回调函数外部调用ws.send()时需注意线程安全不同 WebSocket 库如websockets、aiohttp的 API 和线程模型差异较大请根据实际所用库调整。方案二处理 1006——发送静音帧保活1006 的根因是服务端在一段时间内没收到任何 WebSocket 数据帧实测约 25 秒触发官方未公布精确值。解法很直接在没有真实音频数据时定时发送静音 PCM 帧。OpenAI Realtime API 要求音频格式为 PCM1616-bit little-endian 单声道支持 16kHz 或 24kHz 采样率。以下示例使用 16kHzimport struct, threading, time # 480 samples × (1/16000s) 30msPCM16 16kHz 单声道 SILENCE_FRAME struct.pack( h * 480, *([0] * 480))然后起一个后台线程每 5 秒发一次import threading # 使用 threading.Event 作为停止标志兼容不同 WebSocket 库 stop_event threading.Event() def keepalive_loop(ws, interval5): while not stop_event.is_set(): ws.send(SILENCE_FRAME, opcode0x2) time.sleep(interval)库依赖说明以上代码使用threading.Event作为停止标志避免依赖特定库的ws.connected属性不同库属性名差异较大直接使用可能报AttributeError。请根据实际所用 WebSocket 库调整ws.send()的调用方式。为什么是 5 秒因为服务端阈值实测约 25 秒官方未公布精确值5 秒间隔留足余量。关于计费OpenAI Realtime API 官方文档说明音频 token 按实际发送的音频时长计费并非仅计 VAD 激活段。静音帧是否计费建议以官方最新文档和实际账单为准不应假设纯静音一定不计费。方案三用聚合 API 网关统一管理 WebSocket 连接如果你不想自己维护心跳逻辑有些 API 聚合平台的 WebSocket 代理层据称会帮你做保活。OpenRouter 和 ofox.io 是其中有公开文档可查的两个此外也有一些小型网关服务声称在代理 Realtime API 时会在网关层注入心跳帧客户端不需要自己处理 keepalive。选用前建议核查其文档中是否明确说明支持 Realtime API 的 WebSocket 代理以及心跳注入行为是否有说明。免责说明本文不点名推荐具体小型第三方服务原因是此类服务与作者的利益关系难以独立核实且其是否真实支持 Realtime API 心跳注入同样无法独立验证。如需选用 API 聚合网关建议优先选择有公开文档、社区口碑可查的服务并自行评估。我目前的做法是自己写心跳方案一二因为对延迟比较敏感不想多一跳。但如果你是团队协作、需要统一管理多个 Realtime session 的用量和费用走聚合网关确实省事。完整的保活方案方案一二合并把续期和静音帧心跳合在一起实际生产中我用的代码大概长这样完整类定义可直接复制使用库依赖说明以下代码基于websocket-client库编写使用threading.Event替代ws.connected以提高跨库兼容性。如使用websocketsasyncio或aiohttp需将线程模型改为asyncio协程。import json, time, threading, struct # PCM16 16kHz 单声道30ms 静音帧 SILENCE_FRAME struct.pack( h * 480, *([0] * 480)) class RealtimeKeepAlive: def __init__(self, ws): self.ws ws self.expires_at None self._stop threading.Event() def start(self, expires_at): self.expires_at expires_at threading.Thread(targetself._renewal_loop, daemonTrue).start() threading.Thread(targetself._silence_loop, daemonTrue).start() def stop(self): self._stop.set() def _silence_loop(self): while not self._stop.is_set(): self.ws.send(SILENCE_FRAME, opcode0x2) time.sleep(5) def _renewal_loop(self): # 注意session.update 能否重置 expires_at 尚未经官方文档确认 # 若无效请改用重建连接方案在 expires_at 前主动断开并重连。 while not self._stop.is_set(): if self.expires_at is not None: wait self.expires_at - time.time() - 60 if wait 0: time.sleep(wait) self.ws.send(json.dumps({ type: session.update, session: {} # 至少传空对象实际可填需更新的字段 })) time.sleep(10) # 等 session.updated 回来更新 expires_at这套跑了两周零断连。之前平均每天断 3-4 次现在稳了。两种断连的快速鉴别表特征close code 4008close code 1006含义session expired实测推断abnormal closure触发条件expires_at到期未续实测约 25s 无数据帧官方未公布是否有 close frame有服务端发 4008建议抓包核实无直接 TCP 断开典型场景长对话超过会话最大时长官方记载默认 30 分钟用户长时间沉默/buffer 卡死解法session.update续期待官方确认或重建连接定时发静音帧普通 REST API 会遇到吗不会不会常见问题 FAQQ: OpenAI Realtime API 的 session.expires_at 默认是多久能改吗官方文档记载默认最大会话时长为 30 分钟。截至本文撰写时OpenAI 官方未提供自定义过期时间的参数。唯一的办法是在到期前发session.update续期注该操作能否重置expires_at尚未经官方文档明确确认来自社区帖子请以官方最新文档为准若续期无效可考虑在到期前主动重建连接。Q: 我用 gpt-5.5 的普通 Chat API 从来没遇到 4008为什么 Realtime API 会有因为 gpt-5.5 的/v1/chat/completions是无状态的 HTTP 请求-响应模型不存在会话概念。OpenAI Realtime API 是有状态的 WebSocket 长连接服务端需要维护 session 资源所以有过期机制。两套完全不同的接入方式。Q: 发静音帧会不会被计费不确定。OpenAI Realtime API 官方文档说明音频 token 按实际发送的音频时长计费并非仅计 VAD 激活段。我个人实测连续发静音心跳一周后对应 session 的 audio input token 接近 0但这不能作为官方保证。建议以官方最新文档和实际账单为准。Q: close code 1006 有时候不是 25-30 秒就断了有时候更短什么原因1006 的触发还跟你的网络中间件有关。如果你前面有 Nginx 反代或 Cloudflare它们自己也有 WebSocket idle timeoutNginx 的proxy_read_timeout默认 60sCloudflare 默认约 100s各计划相同。如果中间件的 timeout 比 OpenAI 服务端的阈值还短你会更早收到 1006。排查时先确认中间件配置proxy_read_timeout 300s; proxy_send_timeout 300s;Q: 用聚合网关代理 Realtime API还需要自己写心跳吗看具体网关实现。有些网关在代理层做了 keepalive 注入客户端不需要额外处理。但我建议不管用不用网关客户端都保留心跳逻辑作为兜底——网关挂了你的代码还能 fallback 到直连。反正就几行代码的事。我的最终选择折腾完这一圈结论很简单OpenAI Realtime API 跟传统 REST API 是两个世界不能用调 gpt-5.5 的经验去套。4008 和 1006 这两个 close code 看着像网络问题实际上是 OpenAI 服务端的主动行为客户端必须配合。心跳代码加起来不到 30 行但能省掉无数次怎么又断了的排查时间。如果你也在做语音助手或者实时翻译之类的项目建议一开始就把保活逻辑写进去别等生产环境出问题再补。对于需要在聚合网关后面接入 Realtime API 的场景上述RealtimeKeepAlive类同样适用——网关层的 endpoint 替换后session.created/session.updated事件结构不变心跳逻辑无需改动。