LLM پر مبنی فیچر بنانے والی ہر ٹیم ایک ہی فیصلے کے مقام پر پہنچتی ہے: ماڈل ٹوکنز کو بنتے ہی stream کرتا ہے، اور frontend کو انہیں ایک ایک کر کے ظاہر کرنا ہوتا ہے بجائے اس کے کہ پوری response کا انتظار کیا جائے۔ فوری جواب عموماً یہی ہوتا ہے: “WebSockets، کیونکہ streaming” — اور پھر تین ہفتے بعد کوئی یہ debug کر رہا ہوتا ہے کہ chat UI localhost پر تو بالکل درست کام کرتی ہے مگر production میں ایک ہی بہت بڑے chunk کی صورت میں پہنچتی ہے، کیونکہ nginx proxy یا corporate load balancer نے ایک بھی byte آگے بھیجنے سے پہلے پوری response کو buffer کر لیا تھا۔
اصل فیصلہ “streaming یا non-streaming” کا نہیں ہے — بلکہ یہ کسی خاص data flow کے لیے درست transport منتخب کرنے کا معاملہ ہے، اور WebSockets اکثر غلط جواب ہوتا ہے اس مسئلے کے لیے جو حقیقت میں، real-time UI کے جوش کے نیچے، server سے client تک متن کا یک طرفہ stream ہوتا ہے۔ Server-Sent Events (SSE) خاص طور پر اسی نوعیت کے مسئلے کے لیے موجود ہیں، سادہ HTTP پر چلتے ہیں، اور WebSockets کی لائی ہوئی infrastructure complexity کی ایک پوری قسم سے بچا لیتے ہیں — لیکن SSE کے اپنے بھی تیز کنارے ہیں، خاص طور پر proxy buffering کے گرد، جو ٹیموں کو پہلی بار حیران کرتے ہیں جب وہ کسی bare Uvicorn process سے زیادہ پیچیدہ setup کے پیچھے deploy کرتی ہیں۔ یہ پوسٹ واضح کرتی ہے کہ ان تینوں transports میں سے ہر ایک کب واقعی درست ہے، production infrastructure میں streaming کو کیا چیز توڑتی ہے، اور SSE اور WebSocket token streaming دونوں کے لیے کام کرنے والا FastAPI code کیا ہے۔
آپ یہ سیکھیں گے:
- کیوں polling اب بھی “تقریباً real-time” فیچرز کے ایک اہم حصے کے لیے درست انتخاب ہے، اور کب یہ ناکافی ہو جاتی ہے
- Server-Sent Events سادہ HTTP پر کیسے کام کرتے ہیں، اور کیوں یہ LLM token streaming کے لیے زیادہ بہتر fit ہیں جتنا اکثر ٹیمیں ابتدا میں سمجھتی ہیں
- کب آپ کو واقعی WebSockets کی ضرورت ہوتی ہے — وہ مخصوص bidirectional، low-latency صورتیں جنہیں SSE cover نہیں کر سکتا
- nginx buffering، gzip، اور proxy configuration کی وہ خاموش خرابیاں جو production میں streaming responses توڑ دیتی ہیں
StreamingResponseپر مبنی SSE اور ایک WebSocket endpoint کے لیے کام کرنے والا FastAPI code- ایک فیصلہ سازی framework جسے آپ براہِ راست استعمال کر سکتے ہیں بجائے اس کے کہ جو transport اس وقت سب سے زیادہ trend میں ہو اسی کو default بنا لیں
فہرستِ مضامین
- بنیادی باتیں
- Polling: اب بھی بعض اوقات درست
- Token-by-Token آؤٹ پٹ کے لیے Server-Sent Events
- WebSockets: جب آپ کو واقعی Bidirectional کی ضرورت ہو
- Disconnects اور Mid-Stream Errors کو سنبھالنا
- بڑی ایپ میں Streaming Endpoints کو منظم کرنا
- فیصلہ سازی کا Framework
- Buffering اور Proxy کی وہ خرابیاں جو Streaming توڑ دیتی ہیں
- Code Examples
- عام خرابیاں
- Production کی بہترین طریقہ کار
بنیادی باتیں
تین Transports، مسئلے کی تین مختلف شکلیں
Polling میں client وقفے وقفے سے پوچھتا ہے: “کچھ نیا آیا؟” Server-Sent Events میں server ایک HTTP connection کھلا رکھتا ہے اور جیسے ہی واقعات پیش آتے ہیں متن کی صورت میں انہیں اسی پر نیچے push کرتا ہے، صرف ایک سمت میں، server سے client تک۔ WebSockets ایک full-duplex connection ہے جس میں دونوں طرف سے کسی بھی وقت پیغامات بھیجے جا سکتے ہیں، request/response cycle سے مکمل طور پر آزاد۔
زیادہ تر ٹیمیں جو غلطی کرتی ہیں وہ یہ ہے کہ اسے مشکل کی سیڑھی سمجھ لیتی ہیں — polling بنیادی ہے، SSE بہتر ہے، WebSockets سب سے بہتر — جبکہ حقیقت میں یہ مسئلے کی شکل کا فیصلہ ہے۔ اگر آپ کا data صرف ایک سمت میں بہتا ہے (chat UI کو stream ہوتے LLM tokens، progress bar، live log tail)، تو WebSockets ایسے مسئلے کو حل کر رہا ہے جو آپ کے پاس ہے ہی نہیں، جبکہ ایسی infrastructure شامل کر رہا ہے جسے آپ کو برقرار رکھنا پڑے گا: connection state، reconnection logic، اور ایسا protocol جو standard HTTP tooling کے پیچھے اتنا صاف fit نہیں بیٹھتا۔
خاص طور پر LLM Features کے ساتھ یہ فیصلہ زیادہ اہم کیوں ہے
Token-by-token LLM output اپنی فطرت میں یک طرفہ اور text-based ہوتا ہے — بالکل وہی شکل جس کے لیے SSE ڈیزائن کیا گیا تھا۔ یہ عموماً اس sub-50ms قسم کی latency کے لحاظ سے بھی critical نہیں ہوتا جس کے لیے WebSockets بنے تھے (کوئی token نظریاتی حد سے 100ms بعد پہنچے تو مکمل generation کے کئی سیکنڈ کے دوران یہ محسوس بھی نہیں ہوتا)؛ اصل اہمیت volume اور reliability کی ہوتی ہے — بہت سے tokens، درست ترتیب میں، ایسے connection پر جو مختصر network hiccup کے باوجود پوری exchange کو fail نہ کر دے۔
Polling: اب بھی بعض اوقات درست
Polling کو بہت جلدی رد کر دیا جاتا ہے۔ ایسی status کے لیے جو کم تبدیل ہوتی ہے — جیسے background job کی completion state، یا document processing pipeline کا مرحلہ — ایک client کا ہر چند سیکنڈ بعد poll کرنا بنانا بھی آسان ہے، debug کرنا بھی آسان ہے، اور ہر proxy، firewall، اور CDN سے بغیر کسی خاص configuration کے گزر جاتا ہے، کیونکہ یہ محض بار بار کی جانے والی سادہ HTTP requests ہیں۔
@router.get("/jobs/{job_id}/status")
async def get_job_status(job_id: str):
job = await fetch_job(job_id)
return {"status": job.status, "progress": job.progress}
const interval = setInterval(async () => {
const res = await fetch(`/jobs/${jobId}/status`);
const data = await res.json();
updateProgressBar(data.progress);
if (data.status === "complete") clearInterval(interval);
}, 2000);
Polling اس وقت ناکافی ہو جاتی ہے جب مطلوبہ update frequency اتنی تیز ہو کہ معقول polling ممکن نہ رہے (sub-second)، یا جب polling clients کی تعداد اتنی بڑھ جائے کہ ضائع ہونے والی “کچھ نہیں بدلا” requests واقعی لاگت بن جائیں — اور یہی بالکل وہ صورتحال ہے جو token-by-token LLM output میں ہوتی ہے، جہاں stream کے مفت دیے گئے تجربے کی نقل کے لیے آپ کو ہر active user پر فی سیکنڈ کئی بار poll کرنا پڑے گا۔
Token-by-Token آؤٹ پٹ کے لیے Server-Sent Events
SSE سادہ HTTP ہے جس میں ایک مخصوص response content type (text/event-stream) اور ایک سادہ text format ہوتا ہے جسے browser کا native EventSource API (یا fetch + ReadableStream read loop) bytes آتے ہی incrementally parse کرنا جانتا ہے، بجائے اس کے کہ connection بند ہونے کا انتظار کرے:
data: {"token": "The"}
data: {"token": " quick"}
data: {"token": " brown"}
ہر data: لائن (جسے ایک خالی لائن ختم کرتی ہے) ایک event ہوتی ہے۔ چونکہ یہ محض HTTP ہے، اس لیے درست buffering configuration کے ساتھ یہ standard load balancers اور CDNs سے گزر جاتا ہے، WebSockets کی طرح protocol upgrade کی ضرورت نہیں ہوتی، اور browser کا built-in EventSource API آپ کے لیے last-event-id کے ساتھ automatic reconnection بھی سنبھال لیتا ہے — ایک ایسی سہولت جو ورنہ آپ کو WebSockets کے لیے خود بنانی پڑتی۔
FastAPI میں SSE، StreamingResponse کے ذریعے serve کیا جاتا ہے جو ایک async generator کو wrap کرتا ہے — ہر yield فوری طور پر کھلے connection پر ایک chunk بھیجتا ہے:
from fastapi.responses import StreamingResponse
import json
async def token_stream(prompt: str):
async for chunk in llm_client.stream(prompt):
yield f"data: {json.dumps({'token': chunk})}\n\n"
yield "data: [DONE]\n\n"
@router.get("/chat/stream")
async def chat_stream(prompt: str):
return StreamingResponse(
token_stream(prompt),
media_type="text/event-stream",
headers={"X-Accel-Buffering": "no", "Cache-Control": "no-cache"},
)
Generator کا async for event loop کے ساتھ قدرتی طور پر تعاون کرتا ہے — ہر yield ایک قدرتی suspension point ہوتا ہے، اس لیے اگر LLM provider سست ہو تو وہ دوسرے requests کو اس طرح block نہیں کرتا جیسے synchronous blocking call کرتی ہے (یہی event loop cooperation Why Your FastAPI Endpoint Blocks the Event Loop میں بیان کیا گیا ہے)۔ X-Accel-Buffering: no header اگلے حصے کی جھلک ہے — اس کے بغیر یہی endpoint مقامی طور پر درست دکھائی دے سکتا ہے اور پھر production میں اپنی output ایک ہی بہت بڑے chunk کی صورت میں دے سکتا ہے۔
WebSockets: جب آپ کو واقعی Bidirectional کی ضرورت ہو
WebSockets اپنی complexity اس وقت جائز بناتے ہیں جب client کو اسی کھلے connection پر واپس data بھیجنے کی ضرورت ہو، کسی بھی وقت، ہر message کے لیے نئی HTTP request کے overhead کے بغیر — جیسے collaborative editor جو keystrokes broadcast کرے، multiplayer game کی position updates، یا chat feature جہاں client mid-stream generation کو interrupt کر سکے یا follow-up messages بھیج سکے اور server کو فوراً ردعمل دینا ہو۔
from fastapi import WebSocket, WebSocketDisconnect
@router.websocket("/ws/chat")
async def chat_ws(websocket: WebSocket):
await websocket.accept()
try:
while True:
prompt = await websocket.receive_text()
async for chunk in llm_client.stream(prompt):
await websocket.send_json({"token": chunk})
await websocket.send_json({"done": True})
except WebSocketDisconnect:
pass
یہ SSE مثال سے ملتا جلتا دکھائی دیتا ہے، مگر اب connection حقیقتاً اس بات کی حمایت کرتا ہے کہ client session کے دوران نئی HTTP request کھولے بغیر ایک نیا prompt بھیج دے — یہی وہ اصل capability ہے جو یہاں حاصل کی جا رہی ہے، اس قیمت پر کہ آپ کو اپنی reconnection logic درکار ہوگی (browsers EventSource کی طرح WebSockets کو auto-reconnect نہیں کرتے)، اگر raw text/JSON سے زیادہ structure چاہیے تو اپنی message framing بھی بنانی ہوگی، اور ایسی infrastructure بھی چاہیے جو WebSocket upgrade handshake کو درست طور پر proxy کرے، جس کی out-of-the-box سپورٹ ہر load balancer یا CDN configuration میں موجود نہیں ہوتی۔
زیادہ تر “LLM response کو chat UI تک stream کریں” قسم کے فیچرز میں client کو دراصل mid-stream کچھ بھیجنے کی ضرورت نہیں ہوتی — اگلا user message ایک نئی request ہوتا ہے، اسی کھلے connection پر بھیجا گیا message نہیں — اور یہی وہ صورت ہے جہاں SSE اسی requirement کو نمایاں طور پر کم infrastructure risk کے ساتھ پورا کر دیتا ہے۔
Disconnects اور Mid-Stream Errors کو سنبھالنا
Streaming response کی failure surface عام request/response کے مقابلے میں زیادہ طویل ہوتی ہے — client mid-stream صفحہ چھوڑ سکتا ہے، LLM provider جزوی response بھیجنے کے بعد error دے سکتا ہے، اور اگر ان دونوں کو صراحت سے handle نہ کیا جائے تو آپ کے پاس یا تو orphaned generation work رہ جاتی ہے یا ایسا stream جو صاف طور پر fail ہونے کے بجائے لٹکا رہتا ہے۔
Client disconnects کو detect کرنا۔ FastAPI کا Request object، is_disconnected() فراہم کرتا ہے، جسے طویل چلنے والے generator کے اندر check کرنا مفید ہے تاکہ آپ وہ کام بند کر دیں (اور اس کی قیمت نہ دیں) جو کوئی وصول ہی نہیں کرے گا:
async def token_stream(request: Request, prompt: str):
async for chunk in llm_client.stream(prompt):
if await request.is_disconnected():
break # client navigated away — stop generating
yield f"data: {json.dumps({'token': chunk})}\n\n"
اس check کے بغیر، browser tab چھوڑ دینے سے بنیادی LLM call نہیں رکتا — آپ ایسے tokens کے لیے ادائیگی کرتے اور انہیں generate کرتے رہتے ہیں جنہیں کوئی کبھی نہیں دیکھے گا، اور حقیقی traffic volume پر یہ خرچ تیزی سے بڑھتا ہے۔
Mid-stream errors کو client تک پہنچانا۔ ایک بار پہلا token بھیج دیا جائے تو آپ HTTP status code مزید تبدیل نہیں کر سکتے — response شروع ہو چکی ہوتی ہے۔ جو errors generation کے دوران آئیں، انہیں data event کی صورت میں بھیجنا پڑتا ہے جسے frontend صراحت سے check کرے، HTTP error status پر بھروسہ کرنے کے بجائے:
async def token_stream(prompt: str):
try:
async for chunk in llm_client.stream(prompt):
yield f"data: {json.dumps({'token': chunk})}\n\n"
except LLMProviderError as exc:
yield f"data: {json.dumps({'error': str(exc)})}\n\n"
finally:
yield "data: [DONE]\n\n"
پھر frontend کو ہر آنے والے event میں error key چیک کرنی ہوتی ہے بجائے اس کے کہ یہ فرض کرے کہ ہر event ایک token ہے — یہ ایسی تفصیل ہے جسے پہلی implementation میں چھوڑ دینا آسان ہے اور جو پہلی بار اس وقت سامنے آتی ہے جب production میں LLM provider واقعی mid-response fail کرتا ہے۔
بڑی ایپ میں Streaming Endpoints کو منظم کرنا
Streaming routes اپنے ساتھ اکثر الگ concerns جمع کر لیتی ہیں — provider-specific retry logic، prompt construction، disconnect handling — جنہیں عام CRUD endpoints کے ساتھ router میں ملا کر نہیں رکھنا چاہیے۔ ایسی domain-driven layout میں جیسی FastAPI Project Structure That Survives Growth میں بیان کی گئی ہے، ایک chat یا completions domain جس کے اپنے router.py، service.py، اور generator functions کے لیے مخصوص streaming.py ہوں، اس logic کو transport سے آزاد، الگ تھلگ اور testable رکھتا ہے — وہی token_stream generator ایک SSE route، ایک WebSocket handler، یا ایسے test کے ذریعے چلایا جا سکتا ہے جو اسے براہِ راست iterate کرے، بغیر اس کے کہ LLM-calling logic تینوں entry points میں تین بار دہرائی جائے۔
فیصلہ سازی کا Framework
| Signal | Polling | SSE | WebSockets |
|---|---|---|---|
| Data direction | Request/response | Server → client only | Bidirectional |
| Update frequency | Seconds+ | Sub-second, high volume | Sub-second, high volume |
| Client sends mid-stream? | N/A | No | Yes |
| Works through standard HTTP proxies | Always | Usually, with config | Needs upgrade support |
| Built-in reconnection | N/A (stateless) | Yes (EventSource) | No — build your own |
| Infra complexity | Lowest | Low-medium | Higher |
| Fits LLM token streaming | Poorly (too coarse) | Well | Overkill unless bidirectional |
اگر client کو connection کے دوران کبھی بھی mid-connection data بھیجنے کی ضرورت نہیں پڑتی، تو SSE سے آغاز کریں — LLM streaming کی اصل شکل یہی ہے، اور اس کی operational cost کم ہے۔ WebSockets کی طرف خاص طور پر تب جائیں جب interaction واقعی real time میں دو طرفہ ہو، صرف اس لیے نہیں کہ ایک ایسے فیچر کے لیے یہ زیادہ sophisticated لگتا ہے جو حقیقت میں یک طرفہ ہے۔
Buffering اور Proxy کی وہ خرابیاں جو Streaming توڑ دیتی ہیں
یہی وہ جگہ ہے جہاں SSE deployments حقیقت میں غلط جاتی ہیں، اور یہ تقریباً کبھی FastAPI code میں نہیں ہوتا — بلکہ اس چیز میں ہوتا ہے جو Uvicorn اور client کے درمیان بیٹھی ہوتی ہے۔
nginx response buffering۔ Default طور پر nginx proxied responses کو client کو بھیجنے سے پہلے buffer کرتا ہے، جو streaming کو مکمل طور پر ناکام بنا دیتا ہے — client کو پوری response ایک ساتھ ملتی ہے، جب LLM call ختم ہوتی ہے، incrementally نہیں۔ حل یہ ہے کہ خاص طور پر streaming route کے لیے buffering بند کی جائے:
location /chat/stream {
proxy_pass http://fastapi_upstream;
proxy_buffering off;
proxy_cache off;
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
}
اوپر FastAPI مثال میں دکھایا گیا X-Accel-Buffering: no response header nginx-specific ہے اور nginx config کو براہِ راست چھیڑے بغیر یہی اثر حاصل کرتا ہے — یہ اس وقت مفید ہے جب آپ کے پاس proxy config کا اختیار نہ ہو مگر app کے response headers پر اختیار ہو۔
gzip/compression middleware۔ Response compression کو مؤثر طریقے سے compress کرنے کے لیے مکمل output buffer کرنی پڑتی ہے، جو incremental delivery کے بالکل خلاف ہے۔ Streaming routes کو کسی بھی GZipMiddleware یا reverse-proxy compression configuration سے خارج رکھیں — چھوٹے SSE chunks کے stream کو الگ الگ compress کرنا overhead بھی بڑھاتا ہے جبکہ فائدہ کم ہوتا ہے کیونکہ عموماً ہر token بہت چھوٹا ہوتا ہے۔
Load balancer idle timeouts۔ ایک managed load balancer (مثلاً AWS ALB) کا ایک default idle timeout ہوتا ہے — اکثر 60 seconds — جو ایسے connection کو بند کر دے گا جس پر اتنی دیر تک کوئی نیا byte نہ بھیجا جائے۔ سست LLM response، یا tokens کے درمیان timeout سے طویل وقفہ، خاموشی سے stream ختم کر دیتا ہے۔ یا تو connection کو فعال رکھنے کے لیے وقفے وقفے سے SSE comment-lines (: keep-alive\n\n) بھیجیں، یا خاص طور پر streaming routes کے لیے load balancer کا idle timeout بڑھا دیں۔
Uvicorn worker count اور طویل عرصہ کھلے رہنے والے connections۔ ہر کھلا SSE یا WebSocket connection اپنی پوری مدت تک ایک worker کی توجہ لیے رکھتا ہے۔ متوازی streaming connections کے مقابلے میں بہت کم workers کے ساتھ Uvicorn چلانے کا مطلب یہ ہوتا ہے کہ نئی، غیر متعلقہ requests بھی ان طویل streams کے پیچھے queue میں لگ جاتی ہیں — اپنی worker count (یا Gunicorn جیسے process manager کے ساتھ Uvicorn worker class) کو صرف اوسط request rate نہیں، بلکہ متوقع concurrent stream count کے مطابق طے کریں۔
Client-side buffering۔ کچھ browsers اور HTTP clients، response کے چھوٹے chunks کو JavaScript تک پہنچانے سے پہلے buffer کرتے ہیں۔ کم از کم chunk size بھیجنا (کچھ implementations ابتدائی events میں padding شامل کرتی ہیں) یا ہر yield کے بعد صراحتاً flush کرنا اس بات سے بچاتا ہے کہ stream واضح طور پر شروع ہونے سے پہلے پہلے چند tokens “اٹکے” ہوئے محسوس ہوں۔
Code Examples
Load balancer idle timeouts سے بچنے کے لیے keep-alive pings کے ساتھ ایک مکمل SSE endpoint، اور اس کے مطابق ایک frontend consumer:
import asyncio
import json
async def token_stream(prompt: str):
last_sent = asyncio.get_event_loop().time()
async for chunk in llm_client.stream(prompt):
yield f"data: {json.dumps({'token': chunk})}\n\n"
last_sent = asyncio.get_event_loop().time()
yield "data: [DONE]\n\n"
@router.get("/chat/stream")
async def chat_stream(prompt: str):
return StreamingResponse(
token_stream(prompt),
media_type="text/event-stream",
headers={
"X-Accel-Buffering": "no",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
},
)
const evtSource = new EventSource(`/chat/stream?prompt=${encodeURIComponent(prompt)}`);
evtSource.onmessage = (event) => {
if (event.data === "[DONE]") { evtSource.close(); return; }
const { token } = JSON.parse(event.data);
appendToken(token);
};
ایسی واقعی bidirectional ضرورت کے لیے — جیسے client کا generation کو mid-stream interrupt کرنا — اوپر دکھایا گیا WebSocket handler قدرتی طور پر اس طرح بڑھایا جا سکتا ہے کہ generation loop کے ساتھ ساتھ آنے والے "stop" message کو بھی check کرے، asyncio.wait کو receive coroutine اور generation coroutine دونوں پر استعمال کرتے ہوئے تاکہ دونوں میں سے جو پہلے مکمل ہو، اسے سنبھالا جا سکے۔
عام خرابیاں
غلطی: LLM streaming کے لیے default طور پر WebSockets اختیار کرنا۔ زیادہ تر chat-style فیچرز ہر turn میں یک طرفہ ہوتے ہیں اور اضافی infrastructure کی ضرورت نہیں ہوتی۔ حل: جب تک client کو واقعی اسی کھلے connection پر mid-stream data بھیجنے کی ضرورت نہ ہو، SSE کو default رکھیں۔
غلطی: proxy buffering بند نہ کرنا۔ یہ “streaming locally کام کرتی ہے، production میں ایک chunk کی صورت میں آتی ہے” کی سب سے عام وجہ ہے۔ حل: ہر streaming route کے لیے buffering کو صراحتاً بند کریں (proxy_buffering off, X-Accel-Buffering: no)، اور staging میں اسے حقیقی proxy کے ساتھ verify کریں، صرف bare Uvicorn process کے خلاف نہیں۔
غلطی: load balancer idle timeouts کے مقابلے میں keep-alives بھول جانا۔ ایسا stream جو LB کے idle timeout سے زیادہ دیر رک جائے، mid-response خاموشی سے ختم ہو جاتا ہے۔ حل: وقفے وقفے سے comment-line pings بھیجیں، اور اپنے load balancer کی idle timeout value کو صراحتاً جانیں بجائے اس کے کہ بس یہ سمجھ لیں کہ یہ کافی بڑی ہوگی۔
غلطی: streaming response کو compress کرنا۔ Compression middleware اپنا کام مؤثر طریقے سے کرنے کے لیے buffer کرتی ہے، جس سے incremental delivery ناکام ہو جاتی ہے۔ حل: streaming routes کو gzip/Brotli middleware سے مکمل طور پر خارج رکھیں۔
غلطی: طویل عرصہ کھلے رہنے والے connections کے لیے workers کم مختص کرنا۔ ہر کھلا stream اپنی مکمل مدت کے لیے ایک worker کو مصروف رکھتا ہے؛ بہت کم workers کا مطلب ہے کہ نئی requests فعال streams کے پیچھے queue میں لگ جائیں گی۔ حل: worker count کو اوسط request throughput نہیں، بلکہ متوقع concurrent streaming connections کے مطابق طے کریں۔
Production کی بہترین طریقہ کار
- یک طرفہ streaming کے لیے SSE کو default رکھیں، اور WebSockets کو واقعی bidirectional، low-latency interaction کے لیے مخصوص رکھیں۔
- staging میں اپنے اصل production proxy stack کے پیچھے streaming کو test کریں، صرف bare Uvicorn process کے خلاف نہیں — buffering bugs تب تک نظر نہیں آتیں جب تک راستے میں proxy نہ ہو۔
- Streaming routes پر buffering اور compression کو صراحتاً بند کریں، reverse proxy config میں بھی اور response headers کے ذریعے بھی۔
- طویل streams پر keep-alive pings بھیجیں تاکہ load balancer اور درمیانی proxies کے idle timeouts سے بچا جا سکے۔
- Uvicorn/Gunicorn worker count کو متوازی کھلے streams کے مطابق طے کریں، صرف requests per second کے مطابق نہیں — ایک دیر تک کھلا connection تیز round trip کے مقابلے میں مختلف capacity cost رکھتا ہے۔
خلاصہ
درست streaming transport کا تعین آپ کے data flow کی اصل شکل سے ہوتا ہے، نہ کہ اس بات سے کہ architecture doc میں کون سا آپشن سب سے زیادہ متاثر کن لگتا ہے — اور LLM token-streaming فیچرز کی بھاری اکثریت کے لیے یہ شکل یک طرفہ ہوتی ہے، جو بالکل وہی چیز ہے جس کے لیے SSE بنایا گیا تھا، WebSockets کے مقابلے میں infrastructure cost کے ایک حصے پر۔ ٹوکنز کو درست طور پر stream کرنے والا code عموماً آسان حصہ ہوتا ہے؛ اصل جگہ جہاں production streaming ٹوٹتی ہے وہ proxy اور load balancer configuration ہے جو آپ کی FastAPI app اور browser کے درمیان کھڑی ہوتی ہے، اس لیے اعتماد کرنے سے پہلے وہیں test کریں۔
کیا آپ کے streaming endpoint کو واقعی production proxy کے پیچھے test کیا گیا ہے، یا صرف bare Uvicorn dev server کے خلاف؟
