مواد پر جائیں
14/30باب 14 از 30

آپ کی پہلی production LLM call: streaming، retries، timeouts

ایک جھوٹا provider بنائیں — 429s، اٹکی sockets، آدھے streams — اور دیکھیں client کیا کرتا ہے۔ full jitter: 226 کے مقابل 2.2 سیکنڈ۔

اس صفحے پر

باب 13 ایک ایسے model پر stopwatch کے ساتھ ختم ہوا جسے آپ چھو سکتے تھے۔ weights آپ کی memory میں تھے، KV cache آپ کے اختیار میں تھا کہ enable کریں یا disable، اور جو عدد نکلا — time to first token — آپ کے hardware کی خاصیت تھا۔

اب اسی model کو ایک port کے پیچھے رکھ دیں، جیسا کہ ہر product کرتا ہے، اور وہی عدد دوبارہ پڑھیں۔ یہ اب بھی time to first token ہے، مگر اب یہ کسی ایسی چیز کی خاصیت نہیں رہی جسے آپ control کرتے ہیں۔ اب اس میں TLS handshake، provider کی queue، ایک rate limiter، اور یہ امکان شامل ہے کہ شاید کوئی token کبھی آئے ہی نہیں۔

یہ آخری جملہ ہی اس chapter کا موضوع ہے۔ جو code آپ لکھنے والے ہیں وہ کچھ compute نہیں کرتا۔ وہ connection کھولتا ہے، انتظار کرتا ہے، جو آتا ہے اسے parse کرتا ہے، جب کچھ نہیں آتا تو فیصلہ کرتا ہے کیا کرنا ہے، جب جو آتا ہے وہ error ہو تو دوبارہ فیصلہ کرتا ہے، اور جب user اپنا ارادہ بدل دے تو خود کو cancel کر لیتا ہے۔ ان میں سے ہر چیز وقت کے ساتھ state کے بارے میں ایک فیصلہ ہے، اور ہر ایک کا ایک غلط جواب ہے جو ship ہوتا ہے اور پیسے خرچ کراتا ہے۔

مسئلے کی شکل یہ ہے، ناپی ہوئی، سب کچھ اسی chapter میں:

کیا ہوالاپروا client کیا کرتا ہےاس کی قیمت
server نے socket accept کیا اور کبھی جواب نہیں دیاانتظار کرتا ہےNode کے خود ہار ماننے سے پہلے 300.8 s
key غلط تھی (401)پانچ بار retry کرتا ہے6,325 ms تاخیر، پھر وہی 401
سو clients نے ایک ساتھ rate limit کو hit کیاسب ایک ہی schedule پر retry کرتے ہیںdrain ہونے میں 226 s، بمقابلہ 2.2 s
request timeout ہوئی اور دوبارہ بھیجی گئیاسے دوبارہ بھیجتا ہےprovider جواب دو بار generate کرتا ہے — اور bill بھی کرتا ہے
connection جواب کے بیچ میں drop ہواpartial text دکھاتا ہےایک درست مختصر جواب سے الگ پہچان نہیں ہوتی

ان میں سے کوئی بھی modelling problem نہیں۔ یہ سب ہر LLM product کی پہلی سو lines میں موجود ہوتے ہیں۔

وہ table دوبارہ پڑھیں اور پوچھیں کہ یہ کس طرح کے program کو بیان کرتا ہے۔ یہ connection کو چالیس seconds تک کھلا رکھتا ہے۔ اسے button سے cancellable ہونا چاہیے۔ یہ partial answer جمع کرتا ہے جو display کرنے کے لیے valid ہے اور save کرنے کے لیے invalid۔ اور یہ server process یا edge worker میں چلتا ہے، اس چیز کے پاس جو answer render کرتی ہے، socket پکڑے ہوئے۔

یہ notebook نہیں۔ ایسا نہیں کہ Python یہ نہیں کر سکتی — کر سکتی ہے، اور لوگ کرتے ہیں — بات یہ ہے کہ پچھلے تیرہ chapters نے جو کچھ بنایا وہ ایک مختلف قسم کی چیز تھی۔ Chapters 1 سے 13 نے weights، gradients، logits اور tokenizer bytes پکڑے ہوئے تھے۔ یہاں سے code ایک connection، ایک retry، ایک cancellation، accumulated state اور، آگے چل کر، ایک permission prompt پکڑتا ہے۔ course بالکل اسی seam پر language بدلتا ہے جہاں object بدلتا ہے۔

لہٰذا rule، ایک بار لکھا ہوا:

اگر code کے ہاتھ میں weights، gradients، logits یا tokenizer bytes ہیں تو یہ Python ہے۔ اگر وہ connection پکڑتا ہے، retries کرتا ہے، cancel کرتا ہے، state accumulate کرتا ہے اور permission مانگتا ہے تو یہ TypeScript ہے۔

seam ایک ہی ہے اور یہیں آتا ہے، Chapter 13 اور Chapter 14 کے درمیان۔ تین آزاد معیار اسے یہاں رکھتے ہیں۔

ایک: ecosystem، گنتی کے ساتھ۔ اس course کا بایاں نصف جس چیز کا حوالہ دیتا ہے وہ Python ہے، اور اس syllabus کے لیے audit کیے گئے بارہ courses میں backpropagation کو کسی دوسری language میں سکھانے کی ایک بھی precedent نہیں: micrograd (17.4K stars)، nanoGPT (62.8K)، nanochat (57.8K)، minbpe (10.7K)، PyTorch (102.8K)، transformers (164.9K)۔ باب 5 کو TypeScript میں لکھنا ان sources سے link توڑ دیتا، اور links اس chapter کی آدھی value ہیں جو rank کرنے سے زیادہ reference ہونے کے لیے موجود ہے۔ اس طرف arithmetic الٹ جاتی ہے: Vercel کا ai package ماہانہ 89.4M downloads پر ہے اور خود وہ چیز ship کرتا ہے — ایک tool-calling agent loop، ToolLoopAgent کے طور پر exported — لہٰذا یہ course باب 23 میں جس concept تک پہنچتا ہے اس کی reference implementation TypeScript میں ہے، اگرچہ، جیسا کہ وہ chapter ناپتا ہے، کسی نے اس کے نام پر اتفاق نہیں کیا؛ Mastra 27.7K stars پر ہے؛ اور Anthropic کے SDKs، ایک specification سے generated، TypeScript میں 202 endpoints declare کرتے ہیں جبکہ Python میں 201 — parity، کوئی courtesy port نہیں۔

دو: MCP کا normative source۔ Model Context Protocol specification کا schema ایک schema.ts file ہے۔ باب 26 کا protocol کسی دوسری language میں سکھانے کا مطلب اس کی founding document کا translation سکھانا ہے۔

تین: search demand، obvious guess کی correction کے ساتھ۔ machine learning python internet کا سب سے saturated phrase ہے؛ ai agent typescript کی اپنی healthy tail ہے۔ لیکن “MCP ecosystem mostly TypeScript ہے” صرف اس بات پر true ہے کہ آپ count کیسے کرتے ہیں: official registry npm پر 8,275 servers list کرتی ہے جبکہ PyPI پر 3,603، مگر downloads میں Python جیتتا ہے — mcp کے لیے ماہانہ 287M plus fastmcp کے لیے 72M، بمقابلہ @modelcontextprotocol/sdk کے لیے 195M۔ MCP یہاں واحد واقعی bilingual علاقہ ہے، اسی لیے باب 27 pretend کرنے کے بجائے ایک ہی server دو بار لکھتا ہے۔

تفصیلات دکھائیں

پانچ declared exceptions، تاکہ rule واقعی rule رہے، slogan نہیں۔

Chapters 17، 20 اور 29 میں Python کا دوسرا panel ہے: top-p sampling implement کرنے کے لیے probability vector آپ کے ہاتھ میں چاہیے اور HTTP API کبھی وہ نہیں دیتی؛ fine-tune کی pricing honestly کرنے کا مطلب اسے واقعی run کرنا ہے، اور LoRA adapter nn.Module کی بارہ lines ہے؛ اور lm-eval-harness، HELM، SWE-bench اور τ-bench Python ہیں، اس لیے TypeScript میں evaluation harness backpropagation والی غلطی کا mirror image ہوتا۔ Chapter 27 bilingual ہے، اوپر والی measured وجہ سے۔ باب 28 Markdown ہے، کیونکہ agent skill خود ایک SKILL.md file ہے اور اسے programming language دینا format کو نہ سمجھنے کے برابر ہوتا۔

تیرہ Python chapters discard نہیں ہوئے۔ port کے دوسری طرف وہی ہے جو انہوں نے بنایا، اور یہاں کا آخری section ایک client کو اس سے connect کرتا ہے۔

آپ یہ سب کسی real provider کے خلاف نہیں سیکھ سکتے۔ آپ اسے chosen moment پر 429 دینے، یا ایسی socket دینے کو نہیں کہہ سکتے جو آپ کا connection accept کرے اور کبھی جواب نہ دے، یا ایسا stream جو لفظ کے بیچ میں رک جائے — اور آپ ہر experiment کے لیے pay بھی کر رہے ہوں گے، جبکہ دلچسپ experiments وہ ہیں جو آپ سو بار چلاتے ہیں۔

لہٰذا course کے اس نصف کا پہلا program client نہیں۔ یہ ایک hostile server ہے: plain Node کی چالیس lines جو chat completions endpoint جیسا ہی wire protocol بولتی ہیں اور demand پر misbehave کرتی ہیں۔ اس chapter کا ہر number اسی سے نکلا۔

mock-provider.mjsJS
import { createServer } from "node:http";

const WORDS = "A tide gauge is a device that measures sea level over time .".split(" ");
const CAPACITY = 3;                      // how many requests it will serve at once
let inflight = 0;

const sse = (res, obj) => res.write(`data: ${JSON.stringify(obj)}\n\n`);

createServer(async (req, res) => {
  const url = new URL(req.url, "http://x");

  if (url.pathname === "/hang") return;                        
  if (url.pathname === "/401") { res.writeHead(401); return res.end("{}"); }

  if (inflight >= CAPACITY) {                                  
    res.writeHead(429, { "retry-after": "1" });                
    return res.end(JSON.stringify({ error: { type: "rate_limit_error" } }));
  }
  inflight++;

  const cut = Number(url.searchParams.get("cut") ?? -1);       // abandon after N chunks
  const how = url.searchParams.get("how");                     // "close" = orderly, else reset
  const max = Number(url.searchParams.get("max_tokens") ?? 999);
  const delay = Number(url.searchParams.get("delay") ?? 60);   // ms per token

  res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache" });
  for (let i = 0; i < Math.min(WORDS.length, max); i++) {
    if (i === cut) {                                           
      how === "close" ? res.end() : res.destroy();             
      inflight--; return;                                      
    }
    await new Promise((r) => setTimeout(r, delay));
    sse(res, { choices: [{ delta: { content: (i ? " " : "") + WORDS[i] }, finish_reason: null }] });
  }
  sse(res, { choices: [{ delta: {}, finish_reason: max < WORDS.length ? "length" : "stop" }] });
  res.write("data: [DONE]\n\n");
  inflight--;
  res.end();
}).listen(8787);

چار hostile behaviours، ہر ایک ایک line: /hang socket accept کرتا ہے اور اس پر کبھی write نہیں کرتا؛ /401 key کو refuse کرتا ہے؛ capacity check ایک genuine 429 بناتا ہے genuine Retry-After header کے ساتھ جب تین requests پہلے ہی in flight ہوں؛ اور ?cut=N answer کو halfway چھوڑ دیتا ہے، یا socket reset کر کے یا — &how=close کے ساتھ — اسے orderly way میں close کر کے، جو بہت اہم نکلتا ہے۔ باقی ایک real Server-Sent Events stream ہے: ہر data: line پر ایک JSON object، events کے درمیان blank line، آخر میں string [DONE]۔1

اسے run کریں، اور chapter کا باقی حصہ measurement ہے۔

terminalBASH
node mock-provider.mjs &
curl -N "http://127.0.0.1:8787/v1/chat?max_tokens=3"
TEXT
data: {"choices":[{"delta":{"content":"A"},"finish_reason":null}]}

data: {"choices":[{"delta":{"content":" tide"},"finish_reason":null}]}

data: {"choices":[{"delta":{"content":" gauge"},"finish_reason":null}]}

data: {"choices":[{"delta":{},"finish_reason":"length"}]}

data: [DONE]

request body، اور وہ key جو server سے کبھی باہر نہیں جاتی

اس حصے کا لنک: request body، اور وہ key جو server سے کبھی باہر نہیں جاتی

chat request messages کی list ہے، ہر message کے ساتھ ایک role۔ وہ list model کی پوری state ہے: calls کے درمیان کوئی memory نہیں، اور جو کچھ آپ چاہتے ہیں کہ model جانے، وہ اسی array کے اندر ہونا چاہیے جو آپ اس بار بھیجتے ہیں۔ باب 15 اس بارے میں ہے کہ اس میں کیا ڈالنا ہے اور باب 16 اس بارے میں ہے کہ اس کی cost کیا ہے، اس لیے یہاں صرف shape ہے۔

call.tsTS
const body = {
  model: "gpt-4.1-mini",
  messages: [
    { role: "system", content: "You explain instruments in one sentence." },
    { role: "user", content: "What is a tide gauge?" },
  ],
  stream: true,
  max_tokens: 200,
};

یہ roles decoration نہیں۔ model کے ایک token بھی دیکھنے سے پہلے انہیں باب 11 کے chat template میں render کیا جاتا ہے، اسی لیے غلط role بھیجنا error raise کرنے کے بجائے answer کو خاموشی سے degrade کر دیتا ہے۔

ایک rule جس کی کوئی exception نہیں: API key کبھی client تک نہیں جاتی۔ نہ browser کے لیے prefixed environment variable میں، نہ build-time constant میں، نہ “temporarily”۔ bundle میں key چند دنوں میں کسی اور کے bill پر key ہے۔ browser آپ کے server سے بات کرتا ہے، آپ کا server key رکھتا ہے اور provider سے بات کرتا ہے — اور چونکہ آپ کا server بیچ میں ہے، یہی واحد جگہ ہے جو meter کر سکتی ہے کہ ہر user کیا spend کرتا ہے، اور Chapter 16 کی accounting کو وہیں رہنا چاہیے۔

اب وہ experiment جس پر chapter بنا ہے۔ ایک سوال، ایک mock provider جو 60 ms each پر تیرہ tokens produce کرتا ہے، پوچھنے کے تین طریقے۔

پہلا، streaming کے بغیر۔ client request بھیجتا ہے اور پورے JSON body کا انتظار کرتا ہے۔

TEXT
blocking   first visible =  791 ms   complete =  791 ms   finish_reason = stop

دونوں numbers ایک جیسے ہیں، اور یہی پورا مسئلہ ہے۔ 791 ms تک user کے پاس spinner ہے، اور ایک لفظ بھی پہلے available نہیں تھا — server کے پاس answer byte by byte موجود تھا، مگر اس نے کچھ نہ کہنے کا انتخاب کیا۔

دوسرا، streaming کے ساتھ۔ وہی server، وہی answer، وہی total work۔ فرق parser کا ہے۔

sse.tsTS
export async function* readSSE(res: Response) {
  const reader = res.body!.getReader();
  const decoder = new TextDecoder();
  let buffer = "";
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });   
    let sep: number;
    while ((sep = buffer.indexOf("\n\n")) !== -1) {       
      const event = buffer.slice(0, sep);
      buffer = buffer.slice(sep + 2);
      for (const line of event.split("\n")) {
        if (!line.startsWith("data:")) continue;
        const payload = line.slice(5).trim();
        if (payload === "[DONE]") return;
        yield JSON.parse(payload);
      }
    }
  }
}

وہاں تین details load-bearing ہیں اور اکثر first attempts تینوں skip کر دیتی ہیں۔ buffer اس لیے موجود ہے کہ network chunk کا event سے کوئی تعلق نہیں: ایک read() آدھا event return کر سکتا ہے، یا ڈھائی۔ { stream: true } flag اس لیے موجود ہے کہ multi-byte UTF-8 character دو chunks کے درمیان split ہو سکتا ہے، اور اس کے بغیر accented letter random طور پر replacement character بن جاتا ہے۔ اور events blank line سے separate ہوتے ہیں، newline سے نہیں، اسی لیے loop \n\n ڈھونڈتا ہے۔

TEXT
streaming  first visible =   65 ms   complete =  793 ms   finish_reason = stop

پہلے word تک بارہ گنا faster، اور last تک دو milliseconds slower۔ Streaming کسی چیز کو faster نہیں بناتی۔ یہ بدلتی ہے کہ user انہی 790 ms میں کیا کر رہا ہے: waiting کے بجائے reading۔ یہی پورا فائدہ ہے، یہ بہت بڑا ہے، اور اسی لیے ہر chat product streams کرتا ہے۔

تیسرا، ایک ساتھ بیس clients کے ساتھ۔ mock provider ایک وقت میں تین requests serve کرتا ہے۔ بیس fire کریں:

TEXT
jitter=true  clients=20  server capacity=3
HTTP requests made: 74   429s received: 54   200s: 20
wall clock: 7,100 ms
retries per client: 0 0 0 1 1 1 2 2 2 3 4 3 3 5 4 5 4 5 4 5
every answer identical: true

بیس answers، چوہتر requests، چون rejections۔ کسی نے کچھ lose نہیں کیا، ہر client کو وہی text ملا، اور واحد visible cost time تھا۔ یہ retry policy کا working ہونا ہے۔ باقی chapter ان تین طریقوں کے بارے میں ہے جن سے یہ fail ہو سکتی ہے۔

finish_reason، اور دو endings جو ایک جیسی لگتی ہیں

اس حصے کا لنک: finish_reason، اور دو endings جو ایک جیسی لگتی ہیں

failures سے پہلے، وہ field جسے تقریباً ہر کوئی first pass میں ignore کرتا ہے۔ ہر stream ایک event پر ختم ہوتا ہے جس میں finish_reason ہوتا ہے۔ stop کا مطلب ہے model نے decide کیا کہ وہ done ہے۔ length کا مطلب ہے یہ token ceiling کو hit کر گیا، لہٰذا answer mid-sentence truncated ہے اور یہ model کی غلطی نہیں۔ بعد کے chapters tool_calls (باب 18) اور content filters add کرتے ہیں۔

اب دو endings دیکھیں جن میں فرق naive client نہیں بتا سکتا۔ وہی server، وہی delay، ایک max_tokens سے truncated اور ایک جہاں connection پانچ tokens کے بعد cleanly close ہوتا ہے:

TEXT
max_tokens=5           loop ended NORMALLY   chunks=5  finish_reason=length  text="A tide gauge is a"
socket closed cleanly  loop ended NORMALLY   chunks=5  finish_reason=null    text="A tide gauge is a"
socket destroyed       threw TypeError: terminated (UND_ERR_SOCKET)
                                             chunks=4  finish_reason=null    text="A tide gauge is"

پہلی دو rows غور سے پڑھیں۔ Identical text۔ Identical chunk count۔ دونوں میں کوئی exception نہیں۔ for await loop دونوں بار normally finished، کیونکہ reader کے point of view سے body end ہوئی اور body بس یہی کر سکتی ہے۔ پوری observation میں صرف اتنا فرق ہے کہ ایک finish_reason: "length" carry کرتا ہے اور دوسرا کچھ بھی نہیں۔

لہٰذا rule “streaming کے دوران errors catch کرو” نہیں۔ یہ ہے:

ایک stream جو finish_reason کے بغیر end ہو، end نہیں ہوا۔ وہ رک گیا۔

missing finish_reason کو ہمیشہ failure سمجھیں، اور اس text کو کبھی completed answer کے طور پر persist نہ کریں۔ تیسری row آسان case دکھاتی ہے — destroyed socket throw کرتی ہے، اور in-flight chunk بھی lose کر دیتی ہے، اسی لیے text اوپر کی دو rows سے ایک word کم ہے۔

پانچ status codes جو پانچ مختلف problems ہیں

اس حصے کا لنک: پانچ status codes جو پانچ مختلف problems ہیں

new product کی سب سے expensive عادت ہر provider return کے لیے ایک ہی catch block ہے۔ یہ codes “یہ fail ہوا” کی variations نہیں۔ یہ پانچ instructions ہیں، اور ان میں سے چار ایک دوسرے سے contradict کرتی ہیں۔

statusاس کا مطلبکیا کریںwait?
400آپ کی request malformed ہے — bad JSON، unknown field، context بہت longcode fix کریںکبھی نہیں
401key غلط، missing یا revoked ہےdeployment fix کریںکبھی نہیں
429rate limit: فی minute بہت زیادہ requests، یا بہت زیادہ tokensretry کریںRetry-After، پھر backoff
500provider broken ہےretry کریںbackoff
503provider overloaded ہے — up ہے، full ہےretry کریںbackoff، اور load shed کریں

اہم line 4xx اور باقی کے درمیان ہے۔ 400 یا 401 کو اگر آپ ہزار بار بھی بھیجیں تو وہ بالکل وہی answer return کرتا ہے، کیونکہ attempts کے درمیان کسی طرف کچھ نہیں بدلتا۔ اسے retry کرنا caution نہیں، extra steps کے ساتھ delay ہے۔ Measured: ایک client جو چھ attempts کرتا ہے — exponential backoff کے ساتھ پانچ retries — اور ایک جو پہلے code پڑھتا ہے۔

TEXT
retry everything  ->  6 requests, gave up after 6,325 ms, still HTTP 401
triage first      ->  1 request,  gave up after     4 ms, still HTTP 401

ایک ایسے answer تک پہنچنے کے لیے چھ seconds کا spinner جو چار milliseconds میں available تھا۔ اور یہ mild version ہے: product میں retries عموماً nested ہوتے ہیں — retrying HTTP client کے اندر retrying job runner کے اندر queue جس کی اپنی redelivery ہے — اس لیے چھ seconds ایک permanently broken deployment کے چھ minutes بن جاتے ہیں جو slow deployment جیسا لگتا ہے۔

triage نو lines ہے اور ایک جگہ belong کرتا ہے:

classify.tsTS
export type Verdict = "retry" | "retry-after" | "fatal";

export function classify(status: number): Verdict {
  if (status === 429) return "retry-after";        
  if (status === 408 || status >= 500) return "retry";
  return "fatal";  // 400, 401, 403, 404, 422 — nothing changes by waiting
}

آپ کی list کے لیے دو اور: 402، جسے کچھ providers “آپ کے credits ختم ہو گئے” کے لیے use کرتے ہیں اور جسے retry کے بجائے زیادہ خریدنے کے link والی screen چاہیے، اور 529 یا اس کے vendor-specific equivalents، جو 503 کی طرح behave کرتے ہیں۔

Backoff، اور jitter واقعی کیا خریدتا ہے

اس حصے کا لنک: Backoff، اور jitter واقعی کیا خریدتا ہے

Retry کرنا آسان ہے۔ Retry کب کرنا ہے، یہی وہ حصہ ہے جس کا measurable right answer ہے۔

Exponential backoff standard ہے: base delay wait کریں، ہر failure کے بعد اسے double کریں، ceiling پر stop کریں۔ یہ اس لیے موجود ہے کہ overloaded server بدتر ہو جاتا ہے اگر ابھی fail ہوئے clients فوراً واپس آ جائیں۔

مسئلہ یہ ہے کہ سب ایک ہی starting point سے double کرتے ہیں۔ اگر سو clients ایک ہی moment پر limit کو hit کریں — اور وہ کریں گے، کیونکہ traffic spike یہی ہوتا ہے — تو سب سو 200 ms wait کرتے ہیں، سب سو ساتھ retry کرتے ہیں، سب سو ساتھ fail ہوتے ہیں، اور سب سو 400 ms wait کرتے ہیں۔ retry schedule نے انہیں synchronise کر دیا۔ یہ thundering herd ہے، اور randomness fix ہے۔2

sleep=random(0, min(cap, base2n))\text{sleep} = \mathrm{random}\big(0,\ \min(\text{cap},\ \text{base} \cdot 2^{\,n})\big)

وہ ایک change — interval کے upper end لینے کے بجائے uniformly pick کرنا — full jitter کہلاتی ہے۔ یہ Math.random() کی ایک call ہے، اور اسے believe کرنے کے بجائے measure کرنا چاہیے:

backoff.tsTS
export const backoffNaive = (n: number, base = 200, cap = 20_000) =>
  Math.min(cap, base * 2 ** n);

export const backoffFull = (n: number, base = 200, cap = 20_000) =>
  Math.random() * Math.min(cap, base * 2 ** n);      

سو clients، ایک server جو ایک وقت میں تین serve کرتا ہے، باقی سب identical، ہر ایک کے تین runs:

HTTP requestsrejectionsworst clientbusiest 50 ms windowwall clock
no jitter، run 149139110 tries46 arrivals65.6 s
no jitter، run 278068019 tries72 arrivals245.7 s
no jitter، run 377067018 tries97 arrivals225.6 s
full jitter، run 13242245 tries32 arrivals2.2 s
full jitter، run 23132136 tries31 arrivals2.3 s
full jitter، run 33182186 tries25 arrivals1.8 s

اس table میں دو چیزیں ہیں، اور دوسری اہم ہے۔

پہلی median ہے: 226 seconds against 2.2، تقریباً سو گنا factor، آدھی سے بھی کم requests کے ساتھ۔ busiest retry window بتاتا ہے کیوں۔ jitter کے بغیر، سو میں سے 97 clients اسی 50-millisecond slot کے اندر آ گئے؛ server کے پاس تین تھے، اس لیے 94 reject ہوئے اور اکٹھے sleep پر چلے گئے، اب بھی synchronised، لمبے wait کے ساتھ دوبارہ یہی کرنے کے لیے۔ jitter کے ساتھ وہی سو اسی windows میں تقریباً تیس کے groups میں spread ہوئے اور تقریباً فوراً drain ہو گئے۔

دوسری variance ہے۔ jitter کے بغیر: 65.6 s، 245.7 s، 225.6 s۔ اس کے ساتھ: 2.2، 2.3، 1.8۔ jitter کے بغیر system صرف badly perform نہیں کرتا، unpredictably perform کرتا ہے، کیونکہ outcome microscopic scheduling accidents decide کرتے ہیں کہ سو synchronised clients میں سے پہلے کون سے تین پہنچے۔ production میں اس bug کا signature یہی ہے: endpoint fine، fine، fine، اور پھر چار minutes لیتا ہے، اور آپ کی کوئی change اسے explain نہیں کرتی۔

اور cheapest retry وہ ہے جو کبھی ہوتا ہی نہیں۔ provider کے سامنے concurrency gate لگائیں — ایک counter جو کبھی N سے زیادہ requests کو in flight نہیں ہونے دیتا — اور وہی بیس clients جنہیں 74 requests اور 7.1 seconds چاہیے تھے اس طرح behave کرتے ہیں:

TEXT
client-side gate of 3: 20 HTTP requests, 0 429s, wall 883 ms

بیس answers کے لیے بیس requests، zero rejections، آٹھ گنا faster۔ retry معذرت ہے؛ gate اس معذرت کی ضرورت نہ رہنا ہے۔

جب provider 429 return کرتا ہے تو عموماً آپ کو Retry-After header میں بتاتا ہے کہ کتنی دیر wait کرنا ہے۔3 وہ number advice نہیں: exchange میں provider واحد party ہے جو جانتی ہے کہ اس کی window کب reset ہوتی ہے۔

لہٰذا wait دونوں میں سے بڑا ہے: کبھی Retry-After سے کم نہیں، اور کبھی آپ کے اپنے backoff سے بھی کم نہیں، کیونکہ header آپ کو بتاتا ہے limiter کب معاف کرے گا، یہ نہیں کہ server کے پاس room کب ہو گی۔

wait.tsTS
const header = res.headers.get("retry-after");
const floor = header ? Number(header) * 1000 : 0;   // seconds -> ms
const wait = Math.max(floor, backoffFull(attempt));  

بیس-client run کے unlucky ترین client کا trace دکھاتا ہے کہ header اپنا کام کر رہا ہے۔ اس کے پہلے چار backoff draws سب ایک second سے کم تھے، اور چاروں override ہوئے:

TEXT
t+   26ms  attempt 0  HTTP 429  -> sleep 1000 ms
t+ 1032ms  attempt 1  HTTP 429  -> sleep 1000 ms
t+ 2034ms  attempt 2  HTTP 429  -> sleep 1000 ms
t+ 3046ms  attempt 3  HTTP 429  -> sleep 1000 ms
t+ 4047ms  attempt 4  HTTP 429  -> sleep 2782 ms
t+ 6852ms  attempt 5  HTTP 200  -> sleep 0 ms

دو practical notes۔ Retry-After seconds کی number کے بجائے HTTP date بھی ہو سکتا ہے، اس لیے دونوں parse کریں۔ اور providers ایک ساتھ دو axes پر rate-limit کرتے ہیں — requests per minute اور tokens per minute — اسی لیے long prompts documented request limit سے کافی نیچے reject ہو جاتے ہیں۔ header دونوں cases میں ایک جیسا لگتا ہے؛ fix ایک جیسا نہیں۔

وہ timeout جو کسی نے choose نہیں کیا

اس حصے کا لنک: وہ timeout جو کسی نے choose نہیں کیا

mock provider سے /hang مانگیں۔ وہ connection accept کرتا ہے، اور پھر کچھ بھی نہیں کرتا: نہ headers، نہ body، نہ close۔ یہ exotic نہیں — یہی load balancer کرتا ہے جب اس کے پیچھے process sockets close کیے بغیر die ہو جائے۔

دو clients، ایک فرق:

TEXT
AbortSignal.timeout(5s)   gave up after   5.0 s  (TimeoutError: The operation was aborted due to timeout)
no timeout                gave up after 300.8 s  (TypeError: fetch failed)
                          cause: HeadersTimeoutError UND_ERR_HEADERS_TIMEOUT

Three hundred seconds۔ socket پانچ minutes تک open، request slot occupied، user spinner دیکھتا ہوا، آخر میں generic TypeError جو کچھ نہیں بتاتا کہ کیا ہوا۔ یہ number bug نہیں: یہ Node کا default headers timeout ہے، generic HTTP client کے لیے reasonable اور user-facing request کے لیے catastrophic۔ ہر runtime میں ایسا default ہوتا ہے، زیادہ تر لوگ اسے کبھی look up نہیں کرتے، اور اپنا جاننے کا واحد طریقہ وہی ہے جو ہم نے ابھی کیا: جان بوجھ کر socket hang کریں۔

لہٰذا: ہر outgoing request کو explicit deadline ملتی ہے، آپ کی chosen۔

deadline.tsTS
const res = await fetch(url, {
  method: "POST",
  headers: { "content-type": "application/json", authorization: `Bearer ${key}` },
  body: JSON.stringify(payload),
  signal: AbortSignal.timeout(20_000),   
});

streaming call کے لیے ایک deadline کافی نہیں، کیونکہ دو مختلف failures ہیں۔ پہلا stream کبھی open نہیں ہوتا: کوئی event آتا ہی نہیں، اور دس سے تیس seconds درست ہیں۔ دوسرا stream open ہوتا ہے اور پھر stall ہو جاتا ہے: tokens flowed اور پھر forever رک گئے، socket اب بھی healthy۔ total-duration timeout stalled stream کو long correct answer سے نہیں الگ کر سکتا، اس لیے آپ کو idle timeout چاہیے — ایک timer جو ہر event سے reset ہو، اور صرف تب fire ہو جب، مثلاً، پندرہ seconds تک کچھ نہ آیا ہو۔

Cancellation وہی machinery ہے جو ایک انسان کی طرف point کرتی ہے۔ AbortSignal.timeout اور user کا Stop press کرنا دونوں AbortError کے طور پر آتے ہیں، اس لیے انہیں combine کریں اور record کریں کہ کون سا fired:

cancel.tsTS
const user = new AbortController();
const signal = AbortSignal.any([user.signal, AbortSignal.timeout(20_000)]);
// stopButton.onclick = () => user.abort();

Aborting صفائی سے بڑھ کر وجہ رکھتا ہے: tokens generate اور bill ہو رہے ہیں جبکہ آپ سن نہیں رہے۔ Chapter 16 اس کی قیمت لگاتا ہے۔

اب وہ failure جو time کے بجائے money cost کرتی ہے۔ client پر request timeout ہوتی ہے، اور obvious move اسے دوبارہ بھیجنا ہے — مگر timeout آپ کو یہ نہیں بتاتا کہ server نے اسے receive کیا یا نہیں۔ بہت often کیا ہوتا ہے کہ اس نے receive کیا، اور اب بھی کام کر رہا ہے۔

Measured۔ mock provider کو answer کے لیے 780 ms چاہیے۔ client 300 ms پر give up کرتا ہے اور retry کرتا ہے۔ server count کرتا ہے کہ اس نے واقعی کتنے answers generate کیے، یعنی وہ کیا bill کرتا:

TEXT
idempotency-key: no    attempt 0: TimeoutError after 300 ms  |  attempt 1: TimeoutError after 300 ms
                       answers generated (and billed): 2

idempotency-key: yes   attempt 0: TimeoutError after 300 ms  |  attempt 1: HTTP 200 (replay) id=cmpl_1
                       answers generated (and billed): 1

key کے بغیر: دو full generations، دو بار paid، اور client نے ان میں سے کوئی بھی receive نہیں کیا۔ key کے ساتھ: server نے دوسری request کو وہی request پہچانا اور instantly اس answer کے ساتھ reply کیا جو وہ پہلے ہی produce کر چکا تھا، اس لیے retry نے double charge بھی avoid کیا اور وہ attempt بھی بنی جو finally succeed ہوئی۔

idempotency key ایک unique string ہے جو آپ ہر logical operation کے لیے generate کرتے ہیں — ہر attempt کے لیے نہیں — اور اس کے ہر retry پر unchanged بھیجتے ہیں۔ server key کے against outcome store کرتا ہے اور اسے replay کرتا ہے۔ payment APIs یہی mechanism اسی وجہ سے use کرتی ہیں۔4

idempotent.tsTS
async function send(url: string, payload: unknown) {
  const key = crypto.randomUUID();      // once per turn, not per attempt

  for (let attempt = 0; attempt < 5; attempt++) {
    const res = await fetch(url, {
      method: "POST",
      body: JSON.stringify(payload),
      headers: { "content-type": "application/json", "idempotency-key": key },  
      signal: AbortSignal.timeout(20_000),
    });
    if (res.ok) return res;
    if (classify(res.status) === "fatal") throw new Error(`HTTP ${res.status}`);
    await sleep(backoffFull(attempt));
  }
  throw new Error("out of attempts");
}

دو honest limits۔ ہر provider completions پر idempotency keys support نہیں کرتا، اور جہاں endpoint idempotent نہیں، ایسی POST کے retries کی correct number جو شاید پہلے ہی run ہو چکی ہو zero ہے۔ اور جو stream halfway fail ہو گیا وہ general case میں replayable نہیں: آپ یا تو اسے restart کرتے ہیں اور دوبارہ pay کرتے ہیں، یا partial text رکھتے ہیں اور اسے incomplete mark کرتے ہیں۔ آپ کا product ان میں سے کیا کرتا ہے یہ product decision ہے، networking decision نہیں، اور اسے ارادتاً کرنا چاہیے۔

اس chapter میں لکھے گئے client کو کوئی idea نہیں کہ port کے پیچھے کیا ہے۔ اس کا base URL commercial provider پر point کریں تو یہ trillion parameters کے model سے tokens stream کرتا ہے۔ اسے Chapter 13 کی arithmetic پر built server پر point کریں — وہ model serve کرتے ہوئے جسے آپ نے باب 10 میں pretrained کیا، اس کے KV cache اور quantized weights کے ساتھ — اور وہی code، unchanged، آپ کے بنائے ہوئے model سے tokens stream کرتا ہے۔

switch.tsTS
const BASE = process.env.LLM_BASE_URL ?? "http://127.0.0.1:8000/v1";  

وہ ایک line اس course کا seam ہے۔ اس کے ایک طرف وہ ہے جو پہلے تیرہ chapters نے بنایا؛ دوسری طرف وہ ہے جو اگلے سولہ build کرتے ہیں۔ boundary clean ہے کیونکہ contract HTTP اور SSE ہے، اور دونوں sides ایک دوسرے کے بارے میں کچھ اور نہیں جانتیں۔

یہ notice کرنے کے قابل ہے کہ crossing سے آپ نے کیا lose کیا۔ commercial endpoint کے پیچھے آپ نہ weights control کرتے ہیں، نہ sampling implementation، نہ وہ version جس سے آپ بات کر رہے ہیں، نہ یہ کہ وہ آج صبح changed ہوا یا نہیں۔ آپ contract control کرتے ہیں: messages جو آپ send کرتے ہیں، deadline جو آپ set کرتے ہیں، codes جن میں آپ فرق کرتے ہیں، اور جب کچھ واپس نہیں آتا تو آپ کیا کرتے ہیں۔ یہ Chapter 5 کے مقابلے میں چھوٹی surface ہے، اور ہر remaining chapter اسی کو اچھی طرح use کرنے کے بارے میں ہے۔

اب آپ کے پاس client ہے جو streams کرتا ہے، وقت پر give up کرتا ہے، right things retry کرتا ہے اور wrong ones کبھی retry نہیں کرتا۔ یہ جو send کرتا ہے وہ اب بھی وہی ہے جو آپ نے type کیا۔

Chapter 15 اس content کے بارے میں ہے، اور یہ ایک discipline کے ساتھ آتا ہے۔ internet prompting advice سے بھرا ہے — model کو tip offer کریں، اسے threaten کریں، کہیں deep breath لے — اور اس میں سے تقریباً کچھ بھی measurement کے ساتھ نہیں آتا۔ کچھ techniques output کو بہت move کرتی ہیں، کچھ بالکل نہیں، اور کم از کم ایک classification task کو worse بناتی ہے جبکہ زیادہ tokens cost کرتی ہے۔ کون سی کون ہے یہ انہیں پڑھنے سے obvious نہیں، اور argument سے settle نہیں ہوتا۔

لہٰذا اگلا chapter ایک bench build کرتا ہے: known answers کے ساتھ ساٹھ cases، ایک ہی prompt کی چار variants، بالکل اسی client کے ذریعے parallel run جو آپ نے ابھی لکھا، باب 4 کے confidence intervals کے ساتھ tabulated — کیونکہ بیس cases پر چار variants کچھ بھی distinguish نہیں کرتیں۔ ایک sentence پورے chapter پر govern کرتا ہے: prompt measure ہوتا ہے، debate نہیں۔


اوپر کا ہر number mock provider سے آیا، Node 22 پر loopback interface کے اوپر، اس لیے latencies کسی بھی real network سے زیادہ clean ہیں۔ یہ intentional ہے: measured failures میں سے کوئی network کی وجہ سے نہیں، اور ایک hostile server جسے آپ restart کر سکتے ہیں، real server سے بہتر سکھاتا ہے جس کے لیے pay کرنا پڑے اور جسے آپ break نہ کر سکیں۔

  1. Server-Sent Events، WHATWG HTML Living Standard، section 9.2۔ wire format — data: fields، blank-line-separated events، id: اور retry: — وہیں defined ہے، EventSource interface کے ساتھ۔ EventSource request body یا custom headers نہیں بھیج سکتا، اسی لیے ہر LLM client format کو use کرنے کے بجائے fetch پر ہاتھ سے parse کرتا ہے۔

  2. Brooker, M. Exponential Backoff and Jitter. AWS Architecture Blog (2015)۔ اوپر استعمال ہونے والی “full jitter” formulation کا source، ان simulations کے ساتھ جو دکھاتی ہیں کہ naive version clients کو synchronise کیوں کرتی ہے۔ load queue کرنے کے بجائے shed کرنے کی companion argument Beyer, Jones, Petoff and Murphy (eds.), Site Reliability Engineering (O'Reilly, 2016) کے Handling Overload chapter میں ہے۔

  3. Fielding, R., Nottingham, M. and Reschke, J. (eds.), HTTP Semantics, RFC 9110, section 15، status code classes define کرتا ہے؛ Nottingham, M. and Fielding, R., Additional HTTP Status Codes, RFC 6585 (2012), section 4، 429 Too Many Requests define کرتا ہے۔ Retry-After RFC 9110 section 10.2.3 ہے، اور seconds کی number یا HTTP date دونوں accept کرتا ہے۔

  4. Stripe، Idempotent requests، docs.stripe.com/api/idempotent_requests، 7 September 2026 کو read — contract کا سب سے clear statement: ہر logical operation کے لیے ایک key، stored results replay، conflict returned while first attempt is still in flight — اور pattern provider-independent ہے۔ یہاں استعمال ہونے والی request اور event shapes کے normative references streaming، error codes اور rate limits کے لیے developers.openai.com/api/reference/resources/chat، اور Messages API کے لیے platform.claude.com/docs/en/api/messages ہیں؛ ai-sdk.dev/docs انہی concerns کی library میں wrapped بہترین worked example ہے۔ سب اسی دن read کیے گئے۔


تیار کردہ

David Vicente Campos

NeuraLIA Labs کے بانی اور MyRealFood کے شریک بانی

میں یونیورسٹی آف لیون سے کمپیوٹر انجینئر ہوں۔ میں نے MyRealFood کی مشترکہ بنیاد رکھی، جہاں بطور CTO میں نے وہ ایپ بنائی جسے لاکھوں لوگ بہتر غذا کے لیے استعمال کر چکے ہیں، اور میں نے NeuraLIA Labs قائم کیا، جہاں میں AI مصنوعات بناتا ہوں۔ یہاں میں ان باتوں کے بارے میں لکھتا ہوں جو اس سفر میں مجھے سمجھنی پڑیں، اس طرح جس طرح کاش کسی نے مجھے سمجھائی ہوتیں۔

مصنف کے بارے میں مزید

NeuraLIA Labs کی جانب سے شائع کردہ۔

نئی پوسٹس اپنے ان باکس میں پائیں

AI کی خبریں، گائیڈز اور پروڈکٹ اپ ڈیٹس — جب ہم آپ کے وقت کے قابل کچھ شائع کریں تو ایک مختصر ای میل۔

کورس انڈیکس

Abstract software decision engine with branching paths, probability nodes, and glowing gates.
jev14 منٹ مطالعہ

Jev AI ماڈل فیصلوں کے لیے بنایا گیا ہے، نثر کے لیے نہیں

TypeSafe AI کا Jev اس لیے توجہ کھینچ رہا ہے کہ یہ software intelligence کو احتمال کے مسئلے کے طور پر دیکھتا ہے: درست branch چنیں، confidence منسلک کریں، اور جب code کو فیصلہ چاہیے ہو تو text لکھوانے کے لیے LLM کو ادائیگی سے بچیں۔

Abstract agent runtime sorting documents, memory blocks and pointer nodes inside a bounded context frame.
context-engineering14 منٹ مطالعہ

طویل مدتی AI ایجنٹس کے لیے کانٹیکسٹ انجینئرنگ

طویل عرصے تک چلنے والے ایجنٹس صرف اس لیے ناکام نہیں ہوتے کہ ونڈو چھوٹی ہے۔ وہ اس وقت ناکام ہوتے ہیں جب فائلیں، ٹول آؤٹ پٹس اور پرانی ہسٹری اس کام کو باہر دھکیل دیتی ہیں جسے ایجنٹ نے مکمل کرنا تھا۔

ماڈل چننے کا کام LIA کے سپرد کرنے کے لیے تیار ہیں؟

ہر AI ماڈل ایک ہی جگہ — آج ہی مفت شروع کریں۔