The stream is flowing, text is appearing, and then it simply stops. No error, no exception — just a half-finished sentence sitting on the user's screen. Silent failures like this are the worst kind to chase.
The network is only one suspect. An over-tight timeout, a dropped chunk, a token ceiling, a safety filter firing — each leaves a different signature and needs a different fix. We'll take them one at a time.
Five Reasons Your Stream Might Cut Out
Quick Diagnostic Checklist
- Network timeout — Connection drops before response completes
- Chunk parsing errors — Malformed JSON in the stream
- Token limit exhaustion — Response runs out of tokens mid-generation
- Safety filter triggered — Gemini detects problematic content and halts
- Backpressure mismatch — Client can't keep up with server's send rate
Let's address each one.
Reason 1: Network Timeout
What Timeout Errors Look Like
TimeoutError: The operation timed out
socket.timeout: _ssl.c:997: The handshake operation timed out
The server sent data, but the client didn't receive it before the timeout window closed.
Set Appropriate Timeout Values
Python's requests library has tight default timeouts. For streaming, you need longer:
import requests
import json
from typing import Generator
def stream_gemini_with_timeout(prompt: str, timeout: int = 60) -> Generator[str, None, None]:
"""
Stream response with configurable timeout
Args:
prompt: User's prompt
timeout: Seconds to wait for data (default 60)
"""
url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent"
headers = {
"x-goog-api-key": "YOUR_GEMINI_API_KEY",
"Content-Type": "application/json"
}
payload = {
"contents": [
{
"role": "user",
"parts": [{"text": prompt}]
}
]
}
try:
response = requests.post(
url,
json=payload,
headers=headers,
stream=True,
timeout=timeout # Increase for longer responses
)
response.raise_for_status()
for line in response.iter_lines():
if line:
try:
data = json.loads(line)
if "candidates" in data and data["candidates"]:
candidate = data["candidates"][0]
if "content" in candidate:
for part in candidate["content"]["parts"]:
if "text" in part:
yield part["text"]
except json.JSONDecodeError:
print(f"Failed to parse line: {line}")
except requests.exceptions.Timeout:
print(f"Streaming timed out after {timeout} seconds")
raise
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
raise
# Usage
for chunk in stream_gemini_with_timeout("Tell me about quantum computing"):
print(chunk, end="", flush=True)Server-Sent Events for Robust Streaming
SSE is more resilient than raw HTTP:
from aiohttp import ClientSession
import asyncio
async def stream_gemini_sse(prompt: str, timeout: int = 300):
"""
Async streaming with 5-minute default timeout
"""
async with ClientSession(timeout=timeout) as session:
url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent"
headers = {"x-goog-api-key": "YOUR_GEMINI_API_KEY"}
payload = {
"contents": [{"role": "user", "parts": [{"text": prompt}]}]
}
async with session.post(url, json=payload, headers=headers) as resp:
async for line in resp.content:
try:
data = json.loads(line)
if "candidates" in data:
for part in data["candidates"][0]["content"]["parts"]:
if "text" in part:
yield part["text"]
except (json.JSONDecodeError, KeyError):
continue
# Usage
async def main():
async for chunk in stream_gemini_sse("Explain artificial intelligence"):
print(chunk, end="", flush=True)
asyncio.run(main())Reason 2: Chunk Parsing Errors
Detect and Skip Malformed JSON
Network corruption can garble JSON chunks:
import json
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
def stream_with_error_handling(prompt: str) -> str:
"""
Safely skip malformed chunks instead of crashing
"""
accumulated_text = ""
error_count = 0
max_errors = 5
for line in stream_gemini_with_timeout(prompt):
try:
if isinstance(line, str):
accumulated_text += line
else:
raise ValueError(f"Unexpected chunk type: {type(line)}")
except (json.JSONDecodeError, ValueError) as e:
error_count += 1
logger.warning(f"Chunk error #{error_count}: {e}")
if error_count > max_errors:
logger.error(f"Too many errors. Stopping.")
break
continue
return accumulated_text
# Usage
result = stream_with_error_handling("What is machine learning?")
print(result)Handle Incomplete UTF-8 Sequences
Chunks might arrive with partial UTF-8 characters:
def safe_decode_chunk(chunk: bytes) -> str:
"""
Skip incomplete UTF-8 sequences and retry
"""
try:
return chunk.decode('utf-8')
except UnicodeDecodeError as e:
return chunk[:e.start].decode('utf-8', errors='ignore')Reason 3: Token Limit Exhaustion Mid-Stream
Monitor Token Usage Metadata
Responses include token counts. Watch them:
def stream_with_token_monitoring(prompt: str, max_tokens: int = 8192):
"""
Stop streaming if approaching token limit
"""
total_tokens = 0
accumulated_text = ""
for chunk_data in stream_gemini_with_timeout(prompt):
if "usageMetadata" in chunk_data:
usage = chunk_data["usageMetadata"]
output_tokens = usage.get("outputTokens", 0)
total_tokens += output_tokens
print(f"Tokens: {total_tokens}/{max_tokens}")
if total_tokens >= max_tokens:
print("⚠️ Token limit reached")
break
if "candidates" in chunk_data:
for part in chunk_data["candidates"][0]["content"]["parts"]:
if "text" in part:
accumulated_text += part["text"]
return accumulated_text, total_tokens
# Usage
text, tokens = stream_with_token_monitoring("Write a long essay on AI", max_tokens=5000)
print(f"Generated {tokens} tokens")Request Sufficient Output Tokens Upfront
Always give yourself room:
def create_stream_with_sufficient_tokens(prompt: str, estimated_output: int = 2000):
"""
Reserve enough tokens to avoid mid-stream cutoff
"""
api_params = {
"model": "gemini-2.0-flash",
"contents": [{"role": "user", "parts": [{"text": prompt}]}],
"generation_config": {
"max_output_tokens": estimated_output,
"temperature": 0.7
}
}
# API call with streaming...
passReason 4: Safety Filter Blocking Content
Detect SAFETY Finish Reason
Gemini halts responses it deems harmful:
def stream_with_safety_detection(prompt: str) -> dict:
"""
Monitor and report safety-triggered stops
"""
result = {
"text": "",
"finish_reason": None,
"safety_ratings": []
}
for chunk in stream_gemini_with_timeout(prompt):
if "candidates" in chunk:
candidate = chunk["candidates"][0]
if "finishReason" in candidate:
result["finish_reason"] = candidate["finishReason"]
if candidate["finishReason"] == "SAFETY":
print("⚠️ Response blocked by safety filter")
break
if "safetyRatings" in candidate:
result["safety_ratings"] = candidate["safetyRatings"]
if "content" in chunk["candidates"][0]:
for part in chunk["candidates"][0]["content"]["parts"]:
if "text" in part:
result["text"] += part["text"]
return result
# Usage
output = stream_with_safety_detection("Some prompt")
if output["finish_reason"] == "SAFETY":
print("Safety filter triggered")
else:
print(output["text"])Customize Safety Settings
You can adjust filter sensitivity (use cautiously):
def stream_with_custom_safety(prompt: str):
"""
Adjust safety thresholds per category
"""
safety_settings = [
{
"category": "HARM_CATEGORY_HARASSMENT",
"threshold": "BLOCK_MEDIUM_AND_ABOVE"
},
{
"category": "HARM_CATEGORY_HATE_SPEECH",
"threshold": "BLOCK_ONLY_HIGH"
}
]
# Include in API request...
passReason 5: Backpressure and Processing Speed Mismatch
Control Backpressure with Queueing
When the server sends faster than you process, use a bounded queue:
import asyncio
from asyncio import Queue
class BackpressureControlledStream:
def __init__(self, max_queue_size: int = 10):
self.queue = Queue(maxsize=max_queue_size)
self.max_queue_size = max_queue_size
async def producer(self, prompt: str):
"""Add streamed chunks to queue"""
async for chunk in stream_gemini_sse(prompt):
await self.queue.put(chunk)
await self.queue.put(None) # End signal
async def consumer(self):
"""Read from queue and process"""
accumulated = ""
while True:
queue_size = self.queue.qsize()
if queue_size > self.max_queue_size * 0.8:
print(f"⚠️ Backpressure: {queue_size}/{self.max_queue_size}")
chunk = await asyncio.wait_for(self.queue.get(), timeout=30)
if chunk is None:
break
accumulated += chunk
return accumulated
async def run(self, prompt: str) -> str:
"""Run producer and consumer concurrently"""
producer_task = asyncio.create_task(self.producer(prompt))
consumer_task = asyncio.create_task(self.consumer())
result = await consumer_task
await producer_task
return result
# Usage
async def main():
stream = BackpressureControlledStream(max_queue_size=20)
result = await stream.run("Explain neural networks")
print(result)
asyncio.run(main())Building Robust Reconnection Logic
Auto-Retry on Failure
import time
class ResilientGeminiStream:
def __init__(self, max_retries: int = 3, backoff_factor: float = 2.0):
self.max_retries = max_retries
self.backoff_factor = backoff_factor
def stream_with_retry(self, prompt: str) -> str:
"""
Auto-reconnect if stream interrupts
"""
accumulated_text = ""
attempt = 0
while attempt < self.max_retries:
try:
for chunk in stream_gemini_with_timeout(prompt):
accumulated_text += chunk
return accumulated_text
except (TimeoutError, ConnectionError) as e:
attempt += 1
if attempt >= self.max_retries:
raise RuntimeError(f"Failed after {self.max_retries} retries: {e}")
wait_time = self.backoff_factor ** attempt
print(f"Retrying in {wait_time}s (attempt {attempt}/{self.max_retries})")
time.sleep(wait_time)
return accumulated_text
# Usage
client = ResilientGeminiStream(max_retries=3)
try:
result = client.stream_with_retry("Tell me about quantum physics")
print(result)
except RuntimeError as e:
print(f"Failed: {e}")Checkpoint-Based Recovery
For long streams, save progress periodically:
import json
def stream_with_checkpoints(prompt: str, checkpoint_file: str = "checkpoint.json"):
"""
Save progress so interrupted streams can resume
"""
checkpoint = {}
try:
with open(checkpoint_file, 'r') as f:
checkpoint = json.load(f)
except FileNotFoundError:
pass
accumulated_text = checkpoint.get("text", "")
chunks_processed = checkpoint.get("chunks", 0)
try:
for i, chunk in enumerate(stream_gemini_with_timeout(prompt)):
if i < chunks_processed:
continue # Skip already-processed chunks
accumulated_text += chunk
chunks_processed += 1
# Save checkpoint every 100 chunks
if chunks_processed % 100 == 0:
with open(checkpoint_file, 'w') as f:
json.dump({
"text": accumulated_text,
"chunks": chunks_processed,
"timestamp": time.time()
}, f)
finally:
# Clean up on completion
import os
if os.path.exists(checkpoint_file):
os.remove(checkpoint_file)
return accumulated_textCommon Questions
Q: I get timeout errors constantly. What's wrong?
A: Check these in order:
- Connection stability: Try wired network instead of WiFi
- Timeout setting: Is 60 seconds enough? Use 5 minutes for long responses
- Server status: Check Google Cloud Status
- Firewall: Proxy or firewall blocking connections?
Q: The safety filter blocked my response. How do I work around it?
A: You can't disable it (security policy). Instead, reframe your prompt academically or cautiously.
Q: How do I stop a response before it hits the token limit?
A: Set max_output_tokens low upfront, or monitor finish_reason and stop manually.
Q: Restarted streaming caused duplicate text. How do I fix that?
A: Use checkpoint-based resumption and track processed chunk indices, as shown above.
Wrapping Up
Streaming interruptions come from five distinct sources. Fix each one:
- Timeouts: Increase timeout duration for streaming
- Parsing errors: Implement JSON error handling with retries
- Token exhaustion: Monitor token usage and set
max_output_tokens - Safety blocks: Detect
finishReason: SAFETYand adjust prompts - Backpressure: Use queuing to balance send/receive rates
For deeper streaming patterns, see Gemini Realtime Streaming Advanced Patterns, which covers SSE and WebSocket integration in detail.
For comprehensive error handling beyond streaming, read Error Handling & Retry Patterns.