Python Flask SSE streaming to GC WebSockets causes 1006 Closure on transcript updates

Problem
The streaming breaks when the LLM Gateway pushes tokens faster than the WebSocket buffer flushes. We’re rebuilding the Five9 AI bot logic, and the old script just dumped the full response at the end. Now the requirement is real-time streaming, and the connection drops immediately. The Flask route establishes the SSE link to the LLM Gateway, parses the incremental tokens, and tries to push them back via the WebSockets API. The loop runs hot without any delay.

@app.route('/llm-stream', methods=['POST'])
def llm_stream():
 req = request.json
 resp = requests.post(GC_LLM_ENDPOINT, json={"input": req['text']}, stream=True)
 for chunk in resp.iter_lines():
 if chunk:
 token = json.loads(chunk)['content']
 ws.send(json.dumps({
 "type": "conversation:interaction:transcript",
 "transcript": {"text": token, "author": {"type": "bot"}, "timestamp": int(time.time() * 1000)}
 }))

Client logs show WebSocketError: 1006 Abnormal Closure after exactly 14 messages. The payload size is tiny, around 80 bytes per message. The Five9 bot just sent static text, but this streaming requirement is choking the connection. The transcript update rate seems to be hitting a hard ceiling on the /api/v2/conversations/websockets endpoint. The error log on the Flask side shows ConnectionResetError: [Errno 104] Connection reset by peer. It’s definitely the write speed. The incremental tokens are arriving every 50ms, and the WebSocket write loop doesn’t have a sleep.

Need to throttle the writes or batch them. The Data Action outbound node can’t handle the latency if I buffer too much. Is there a max write rate for transcript updates? The docs don’t mention a rate limit. The SSE response from Flask also drops tokens if the client disconnects mid-stream. Tried adding a time.sleep(0.1) in the loop, but that introduces a noticeable lag on the client UI. The transcript updates arrive in clumps instead of flowing. The WebSocket client shows the connection is open, but the messages pile up in the send buffer until the OS drops them. The ConnectionResetError suggests the server is closing the socket due to backpressure. Maybe the /api/v2/conversations/websockets endpoint has a hidden rate limit per second? The Five9 websocket endpoint never complained about write speed. This GC implementation feels fragile. The buffer keeps overflowing and the transcript gets corrupted. No idea how to batch without losing real-time feel. The migration deadline is Friday and this is the last blocker. Got any ideas on the throttling logic. The token parsing is fine, it’s the send rate that kills the socket. Five9 handled this with a simple queue, but the GC websocket is way more sensitive to burst writes.

import asyncio
import websockets
import json

async def push_transcript(ws_uri, token_stream):
 async with websockets.connect(ws_uri, ping_interval=20, ping_timeout=10) as ws:
 buffer = ""
 CHUNK_LIMIT = 256
 for token in token_stream:
 buffer += token
 if len(buffer) >= CHUNK_LIMIT:
 payload = {
 "EVENT_TYPE": "transcript_update",
 "TEXT_PAYLOAD": buffer,
 "SESSION_ID": "sess_7721"
 }
 await ws.send(json.dumps(payload))
 buffer = ""
 await asyncio.sleep(0.05)
 if buffer:
 await ws.send(json.dumps({"TEXT_PAYLOAD": buffer}))

You’re hitting the 1006 closure because the socket drops when the backpressure queue overflows. Genesys Cloud’s event stream expects throttled chunks, not a raw token firehose. Set the PING_INTERVAL and CHUNK_LIMIT explicitly. You don’t want the platformClient dropping connections that ignore pings. You’ll also need to pass the WEBSOCKET_AUTH_TOKEN in the URI query string. Grab it using the communications:events:read scope before initializing the connection.

Never send raw newline characters in the TEXT_PAYLOAD field. The routing engine will reject it and kill the socket.

Switch to the /api/v2/communications/events endpoint for your outbound stream if you’re hitting rate limits on the standard WebSocket. It handles the backpressure automatically. Just make sure your STREAM_THROTTLE_MS matches the LLM gateway output speed. The hybrid platform routing will queue the excess anyway. It’s a bit finicky with high throughput.

Check the WEBSOCKET_MAX_FRAME_SIZE in your Flask config. It defaults to 1MB but the GC proxy caps it at 64KB for text frames. Adjust that and the closure stops. The proxy drops it anyway.

Are you routing those transcript updates through the standard ADMIN UI or bypassing the platform entirely? It’s pointless to fight the WEB SOCKET BUFFER with raw tokens. You don’t need manual chunking. Just toggle the CHUNKING POLICY in the ADMIN UI. Let the system handle the flush.

CHUNKING_POLICY: adaptive
ADMIN_UI_OVERRIDE: true
import time
import json

def safe_token_push(ws, token_stream):
 buffer = ""
 for chunk in token_stream:
 buffer += chunk
 if len(buffer) >= 128 or chunk.endswith("."):
 payload = {
 "event": "transcript_update",
 "data": {"text": buffer, "session_id": "sess_7721"}
 }
 ws.send(json.dumps(payload))
 buffer = ""
 time.sleep(0.1)

Cause: The 1006 closure happens because the Flask SSE stream pushes raw tokens faster than the Genesys Cloud WebSocket handshake can acknowledge. The suggestion above about chunking is correct, but the platform still expects a specific payload structure for transcript updates. In IC we used to force a 100ms throttle on the outbound queue. Otherwise the media server just drops the socket. The same logic apply here. Buffer overflow when you send partial tokens without metadata. It crashes the connection instantly.

Solution: Wrap the token stream in a controlled generator and match the exact schema Genesys expects. Add a manual throttle and flush the buffer only when the sentence boundary hits. You’ll need to disable the default keepalive ping in the admin console, it’s fighting with the custom stream. Found a similar workaround in that PII masking thread from last week. The routing engine just needs breathing room between pushes. Set the flush interval to 150ms and the connection stays alive. Don’t forget to validate the JSON structure or the gateway will reject it.

The whole setup feels like duct tape on a pressure valve. You’re forcing a sync Flask route to block an async WebSocket connection, which is exactly why the gateway drops the socket with a 1006 closure. The platform’s presence gateway expects a steady ping-pong heartbeat and will hard-close any connection that stalls past three seconds. Throwing time.sleep or manual chunking just starves the event loop and triggers the internal throttle. You need to offload the token stream to an asyncio queue and respect the backpressure limits before pushing to the GC endpoint.

  • Drop the sync Flask handler and switch to an async endpoint or run the consumer in a separate thread.
  • Implement a bounded asyncio.Queue to buffer tokens and apply backpressure when the queue hits capacity.
  • Send the payload using the official genesys-cloud-py WebSocket client instead of raw websockets, since it handles the required PING/PONG frames and payload schema validation automatically.
import asyncio
import json
from genesyscloud.websockets import WebsocketClient

async def stream_tokens(token_gen, ws_client):
 buffer = ""
 queue = asyncio.Queue(maxsize=50)
 async for token in token_gen:
 buffer += token
 await queue.put(buffer)
 buffer = ""
 if queue.full():
 await asyncio.sleep(0.05)
 while not queue.empty():
 payload = {"event": "transcript_update", "data": {"text": queue.get_nowait()}}
 await ws_client.send(json.dumps(payload))

The SDK handles the connection lifecycle. Stop reinventing the flush logic.