सामग्री पर जाएँ
14/30अध्याय 14 / 30

आपकी पहली Production LLM Call: Streaming, Retries, Timeouts

ऐसा provider बनाएँ जो 429s, अटके sockets और आधे कटे streams से झूठ बोले — और client का व्यवहार मापें। Full jitter: 226 के मुकाबले 2.2 सेकंड।

इस पेज पर

Chapter 13 एक ऐसे model पर stopwatch के साथ खत्म हुआ था जिसे आप छू सकते थे। weights आपकी memory में थे, KV cache को enable या disable करना आपके हाथ में था, और जो संख्या निकली — time to first token — वह आपके hardware की property थी.

अब उसी model को एक port के पीछे रखिए, जैसा हर product करता है, और वही संख्या फिर पढ़िए। यह अब भी time to first token है, लेकिन अब यह किसी ऐसी चीज़ की property नहीं है जिसे आप control करते हैं। इसमें अब TLS handshake, provider की queue, rate limiter, और यह संभावना शामिल है कि कोई token कभी आए ही नहीं।

वही आखिरी clause इस chapter का विषय है। जो code आप लिखने वाले हैं, वह कुछ compute नहीं करता। वह connection खोलता है, इंतज़ार करता है, जो आता है उसे parse करता है, जब कुछ नहीं आता तो क्या करना है यह तय करता है, जब जो आता है वह error होता है तो फिर तय करता है, और जब user अपना मन बदलता है तो खुद को cancel कर देता है। इनमें से हर एक समय के साथ state पर निर्णय है, और हर एक का एक गलत जवाब है जो ship होकर पैसे खर्च कराता है।

समस्या का आकार यह है, मापा हुआ, और यह सब इसी chapter में है:

क्या हुआcareless client क्या करता हैइसकी कीमत
server ने socket accept किया और कभी reply नहीं कियाइंतज़ार करता हैNode के अपने-आप हार मानने से पहले 300.8 s
key गलत थी (401)पाँच बार retry करता है6,325 ms की delay, फिर वही 401
सौ clients ने साथ में rate limit hit कीसभी उसी schedule पर retry करते हैंdrain होने में 226 s, बनाम 2.2 s
request timed out हुई और फिर से भेजी गईउसे फिर भेजता हैprovider answer दो बार generate — और bill — करता है
connection answer के बीच में drop हुआpartial text दिखाता हैसही short answer से अलग पहचानना असंभव

इनमें से कोई भी modelling problem नहीं है। ये सभी अब तक लिखे गए हर LLM product की पहली सौ lines में होते हैं।

उस table को फिर पढ़िए और पूछिए कि यह किस तरह के program का वर्णन करता है। यह चालीस seconds तक connection खुला रखता है। इसे button से cancellable होना चाहिए। यह partial answer accumulate करता है जो 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 पर भाषा बदलता है जहाँ object बदलता है।

तो नियम, एक बार लिखा हुआ:

अगर code के हाथ में weights, gradients, logits या tokenizer bytes हैं, तो वह Python है। अगर वह connection पकड़े है, retries करता है, cancel करता है, state accumulate करता है और permission माँगता है, तो वह TypeScript है।

Seam एक ही है और यह यहीं आता है, Chapter 13 और Chapter 14 के बीच। तीन स्वतंत्र criteria इसे यहाँ रखते हैं।

एक: ecosystem, गिनती के साथ। इस course का left half जिन भी चीज़ों को cite करता है वे Python हैं, और इस syllabus के लिए audit किए गए बारह courses में backpropagation को किसी दूसरी भाषा में सिखाने का एक भी precedent नहीं है: micrograd (17.4K stars), nanoGPT (62.8K), nanochat (57.8K), minbpe (10.7K), PyTorch (102.8K), transformers (164.9K)। Chapter 5 को TypeScript में लिखना उन sources से link तोड़ देता, और links उस chapter के आधे मूल्य हैं जो rank करने के बजाय reference होने के लिए मौजूद है। इस तरफ arithmetic उलट जाती है: Vercel का ai package महीने में 89.4M downloads पर है और चीज़ को खुद ship करता है — tool-calling agent loop, ToolLoopAgent के रूप में exported — इसलिए जिस concept तक यह course Chapter 23 में पहुँचता है, उसका 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 है। Chapter 26 के protocol को किसी दूसरी भाषा में सिखाने का अर्थ है उसके founding document का translation सिखाना।

तीन: search demand, obvious guess में correction के साथ। machine learning python internet पर सबसे saturated phrase है; ai agent typescript की अपनी healthy tail है। लेकिन “MCP ecosystem mostly TypeScript है” केवल इस पर निर्भर करते हुए सच है कि आप कैसे गिनते हैं: official registry npm पर 8,275 servers list करती है, PyPI पर 3,603 के मुकाबले, जबकि downloads में Python जीतता है — mcp के लिए महीने में 287M plus fastmcp के लिए 72M, @modelcontextprotocol/sdk के लिए 195M के मुकाबले। MCP यहाँ सचमुच bilingual territory है, इसलिए Chapter 27 दिखावा करने के बजाय वही server दो बार लिखता है।

विवरण दिखाएँ

पाँच घोषित exceptions, ताकि नियम नियम रहे, slogan नहीं।

Chapters 17, 20 और 29 में Python में दूसरा panel है: top-p sampling implement करने के लिए probability vector आपके हाथ में होना चाहिए और HTTP API आपको वह कभी नहीं देती; fine-tune की pricing ईमानदारी से करने का मतलब है एक चलाना, और LoRA adapter nn.Module की दर्जन भर lines है; और lm-eval-harness, HELM, SWE-bench और τ-bench Python हैं, इसलिए TypeScript में evaluation harness backpropagation वाली गलती का mirror image होगा। Chapter 27 ऊपर मापे गए कारण से bilingual है। Chapter 28 Markdown है, क्योंकि agent skill एक SKILL.md file ही है और उसे programming language देना format को न समझना होगा।

तेरह Python chapters discard नहीं किए गए हैं। port के दूसरी तरफ वही है जो उन्होंने बनाया, और यहाँ का आखिरी section client को उसी से जोड़ता है।

आप यह सब किसी real provider के खिलाफ नहीं सीख सकते। आप उससे चुने हुए moment पर 429 नहीं माँग सकते, या ऐसा socket नहीं माँग सकते जो आपका connection accept करे और कभी answer न दे, या ऐसा stream जो किसी word के बीच में रुक जाए — और आप हर experiment के लिए pay भी कर रहे होंगे, जबकि interesting experiments वही हैं जिन्हें आप सौ बार चलाते हैं।

इसलिए course के इस half का पहला program client नहीं है। यह एक hostile server है: plain Node की चालीस lines जो chat completions endpoint जैसा ही wire protocol बोलती हैं और demand पर misbehave करती हैं। इस chapter की हर संख्या उसी से निकली है।

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 Retry-After header के साथ genuine 429 produce करता है जब तीन requests पहले से in flight हों; और ?cut=N answer को आधे रास्ते छोड़ देता है, या तो socket reset करके या — &how=close के साथ — उसे orderly तरीके से close करके, जो बहुत मायने रखता है। बाकी एक real Server-Sent Events stream है: हर data: line पर एक JSON object, events के बीच blank line, अंत में string [DONE]1

इसे चलाइए, और 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 है, हर एक role के साथ। वही list model की पूरी state है: calls के बीच कोई memory नहीं है, और आप model को जो भी जानने देना चाहते हैं वह इस बार भेजी जाने वाली array के अंदर होना चाहिए। Chapter 15 इस बारे में है कि उसमें क्या डालना है और Chapter 16 इस बारे में है कि उसकी कीमत क्या है, इसलिए यहाँ केवल 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 देखने से पहले उन्हें Chapter 11 के chat template में render किया जाता है, इसलिए गलत role भेजना error raise करने के बजाय answer को चुपचाप degrade करता है।

एक नियम बिना 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 प्रति token पर तेरह tokens produce करता है, पूछने के तीन तरीके।

पहला, बिना streaming के। client request भेजता है और पूरे JSON body का इंतज़ार करता है।

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

दोनों numbers समान हैं, और यही पूरी समस्या है। 791 ms तक user के पास spinner है, और एक भी word पहले उपलब्ध नहीं था — server के पास answer था, byte दर 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, या ढाई events return कर सकता है। { stream: true } flag इसलिए है क्योंकि multi-byte UTF-8 character दो chunks में split हो सकता है, और इसके बिना accented letter random रूप से replacement character बन जाता है। और events blank line से अलग होते हैं, newline से नहीं, इसलिए loop \n\n ढूँढता है।

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

पहले word तक बारह गुना तेज, और last तक दो milliseconds धीमा। Streaming किसी चीज़ को तेज नहीं बनाती। यह बदलती है कि user उन्हीं 790 ms के दौरान क्या कर रहा है: इंतज़ार के बजाय पढ़ना। यही पूरा benefit है, यह बहुत बड़ा है, और यही कारण है कि हर chat product stream करता है।

तीसरा, एक साथ बीस 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। किसी ने कुछ नहीं खोया, हर client को वही text मिला, और केवल visible cost time थी। यह retry policy के काम करने का उदाहरण है। इस chapter का बाकी हिस्सा उन तीन तरीकों के बारे में है जिनसे यह fail हो सकती है।

finish_reason, और दो endings जो समान दिखती हैं

सेक्शन का लिंक: finish_reason, और दो endings जो समान दिखती हैं

Failures से पहले, वह field जिसे लगभग हर कोई पहले pass में ignore करता है। हर stream एक event के साथ खत्म होता है जो finish_reason carry करता है। stop का अर्थ है model ने तय किया कि वह done है। length का अर्थ है वह token ceiling से टकराया, इसलिए answer mid-sentence truncated है और यह model की गलती नहीं है। बाद के chapters tool_calls (Chapter 18) और content filters जोड़ते हैं।

अब दो 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. किसी भी case में exception नहीं। for await loop दोनों बार normally finish हुआ, क्योंकि reader के point of view से body ended और body बस इतना ही कर सकती है। पूरे observation में एकमात्र difference यह है कि एक finish_reason: "length" carry करता है और दूसरा कुछ भी नहीं।

तो rule “streaming के दौरान errors catch करो” नहीं है। यह है:

कोई stream जो finish_reason के बिना खत्म होता है, खत्म नहीं हुआ। वह रुक गया।

Missing finish_reason को हमेशा failure मानिए, और उस text को कभी completed answer के रूप में persist मत कीजिए। तीसरी row आसान case दिखाती है — destroyed socket throw करता है, और in-flight chunk भी खो देता है, इसलिए text ऊपर के दोनों से एक word छोटा है।

किसी नए product की सबसे महँगी आदत है provider द्वारा return की गई हर चीज़ के लिए एक catch block। ये codes “it failed” की variations नहीं हैं। ये पाँच instructions हैं, और उनमें से चार एक-दूसरे का विरोध करते हैं।

statusइसका मतलबक्या करेंwait?
400आपकी request malformed है — bad JSON, unknown field, context बहुत लंबाcode ठीक करेंकभी नहीं
401key गलत, missing या revoked हैdeployment ठीक करेंकभी नहीं
429rate limit: प्रति minute बहुत अधिक requests, या बहुत अधिक tokensretryRetry-After, फिर backoff
500provider टूट गयाretrybackoff
503provider overloaded है — वह up है, full हैretrybackoff, और load shed करें

महत्वपूर्ण line 4xx और बाकी के बीच चलती है। 400 या 401 को अगर आप हजार बार भेजें तो ठीक वही answer लौटेगा, क्योंकि attempts के बीच किसी भी end पर कुछ नहीं बदलता। उसे retry करना caution नहीं, extra steps वाली delay है। मापा गया: एक client जो six 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

चार milliseconds में उपलब्ध answer तक पहुँचने के लिए छह seconds का spinner। और यह mild version है: product में retries आमतौर पर nested होते हैं — retrying HTTP client के अंदर retrying job runner, उसके अंदर अपनी redelivery वाली queue — इसलिए छह seconds छह minutes बन जाते हैं, और permanently broken deployment 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 “you are out of credit” के लिए use करते हैं और जिसे retry के बजाय और खरीदने के link वाली screen चाहिए, और 529 या उसके vendor-specific equivalents, जो 503 की तरह behave करते हैं।

Retry करना आसान है। कब retry करना वह हिस्सा है जिसका measurable right answer है।

Exponential backoff standard है: base delay wait करें, हर failure के बाद उसे double करें, ceiling पर रोक दें। यह इसलिए मौजूद है क्योंकि overloaded server और worse होता है अगर जो clients अभी fail हुए वे तुरंत लौट आएँ।

Problem यह है कि सब उसी 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)

वह single change — upper end लेने के बजाय interval से uniformly pick करना — full jitter कहलाता है। यह Math.random() की एक call है, और इसे मानने के बजाय 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 बनाम 2.2, लगभग सौ गुना factor, आधे से भी कम requests के साथ। busiest retry window बताती है क्यों। jitter के बिना, सौ में से 97 clients तक उसी 50-millisecond slot में पहुँचे; server के पास तीन थे, इसलिए 94 reject हुए और साथ में सो गए, अभी भी synchronised, फिर longer wait के साथ वही करने के लिए। jitter के साथ वही सौ clients उन्हीं windows में लगभग thirty के groups में फैल गए और लगभग तुरंत drain हो गए।

दूसरी है variance। बिना jitter: 65.6 s, 245.7 s, 225.6 s। उसके साथ: 2.2, 2.3, 1.8। jitter के बिना system केवल खराब perform नहीं करता, unpredictable perform करता है, क्योंकि outcome microscopic scheduling accidents से तय होता है जो synchronized सौ clients में से कौन से तीन पहले पहुँचते हैं यह चुनते हैं। production में इस bug की signature यही है: एक endpoint जो fine, fine, fine है, और फिर चार minutes लेता है, और आपकी कोई change उसे explain नहीं करती।

और सबसे सस्ता retry वह है जो होता ही नहीं। provider के सामने एक concurrency gate रखिए — ऐसा counter जो N से अधिक requests को in flight नहीं होने देता — और वही twenty 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, आठ गुना तेज। Retry apology है; gate वह है जिसमें apology की जरूरत ही नहीं पड़ती।

जब provider 429 return करता है तो आमतौर पर वह आपको Retry-After header में बताता है कि कितनी देर wait करना है।3 वह number advice नहीं है: exchange में provider ही अकेली party है जिसे पता है कि उसका window कब reset होता है।

इसलिए wait दोनों में से बड़ा है: Retry-After से कभी कम नहीं, और आपके अपने backoff से भी कभी कम नहीं, क्योंकि header आपको बताता है कि limiter आपको कब forgive करता है, यह नहीं कि 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));  

Twenty-client run में unluckiest client का trace दिखाता है कि header अपना काम कर रहा है। उसके पहले चार backoff draws सभी one second से कम थे, और चारों overridden हुए:

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 HTTP date भी हो सकता है, seconds की संख्या नहीं, इसलिए दोनों parse करें। और providers एक साथ दो axes पर rate-limit करते हैं — requests per minute और tokens per minute — इसलिए long prompts documented request limit से बहुत नीचे reject हो जाते हैं। Header दोनों cases में समान दिखता है; fix समान नहीं है।

mock provider से /hang माँगिए। वह connection accept करता है, और फिर कुछ भी नहीं करता: न headers, न body, न close। यह exotic नहीं है — load balancer यही करता है जब उसके पीछे की process sockets close किए बिना मर गई हो।

दो clients, एक difference:

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

तीन सौ seconds। पाँच minutes तक socket खुला पकड़ा हुआ, 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 मिलता है, जिसे आप चुनते हैं।

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 आता ही नहीं, और ten to thirty seconds सही है। दूसरा है stream open होता है और फिर stall हो जाता है: tokens flow हुए और फिर रुक गए, हमेशा के लिए, socket अभी भी healthy। Total-duration timeout stalled stream और long correct answer में फर्क नहीं बता सकता, इसलिए आपको चाहिए idle timeout — ऐसा timer जो हर event से reset हो, और तभी fire हो जब, मान लीजिए, पंद्रह seconds तक कुछ न आया हो।

Cancellation वही machinery है जो किसी person की तरफ point करती है। AbortSignal.timeout और user का Stop दबाना दोनों AbortError के रूप में आते हैं, इसलिए उन्हें combine करें और record करें कि कौन fire हुआ:

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

Aborting केवल tidiness से आगे की वजह से मायने रखता है: जब आप सुन नहीं रहे होते तब भी tokens generate और bill हो रहे होते हैं। Chapter 16 उसकी कीमत लगाता है।

अब वह failure जो time के बजाय money खर्च कराता है। Client पर request timeout होती है, और obvious move है उसे फिर भेजना — लेकिन timeout आपको यह नहीं बताता कि server ने उसे receive किया या नहीं। बहुत बार उसने किया होता है, और अभी भी काम कर रहा होता है।

मापा गया। 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 को उनमें से कोई भी नहीं मिला। Key के साथ: server ने second request को उसी request के रूप में पहचाना और उस answer के साथ तुरंत reply किया जो वह पहले ही produce कर चुका था, इसलिए retry ने double charge से बचाया और वही attempt आखिरकार succeed हुआ।

Idempotency key एक unique string है जिसे आप प्रति logical operation generate करते हैं — प्रति attempt नहीं — और उसके हर retry पर unchanged भेजते हैं। server outcome को key के against store करता है और replay करता है। Payment APIs इसी reason से यही 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 की संख्या zero है जो already run हो सकता है। और जो 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 पर बने server पर point कीजिए — उस model को serve करते हुए जिसे आपने Chapter 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";  

वह single line इस course का seam है। उसके एक तरफ वह है जो पहले तेरह chapters ने बनाया; दूसरी तरफ वह है जो अगले सोलह बनाते हैं। Boundary clean है क्योंकि contract HTTP और SSE है, और दोनों sides एक-दूसरे के बारे में कुछ और नहीं जानते।

यह notice करना worth है कि crossing से आपने क्या खोया। Commercial endpoint के पीछे आप न weights control करते हैं, न sampling implementation, न वह version जिससे आप बात कर रहे हैं, न यह कि वह आज सुबह बदल गया या नहीं। आप contract control करते हैं: जो messages आप भेजते हैं, जो deadline आप set करते हैं, जिन codes को आप distinguish करते हैं, और जब कुछ वापस नहीं आता तो आप क्या करते हैं। यह Chapter 5 में आपके पास मौजूद surface से छोटा surface है, और बाकी हर chapter इसे अच्छी तरह use करने के बारे में है।

अब आपके पास ऐसा client है जो stream करता है, समय पर give up करता है, सही चीज़ों को retry करता है और गलत चीज़ों को कभी retry नहीं करता। वह जो भेजता है, वह अभी भी बस वही है जो आपने type किया।

Chapter 15 उसी content के बारे में है, और वह discipline के साथ आता है। Internet prompting advice से भरा है — model को tip offer करें, उसे threaten करें, उसे deep breath लेने को कहें — और लगभग कोई भी measurement के साथ नहीं आता। इनमें से कुछ techniques output को बहुत move करती हैं, कुछ बिल्कुल नहीं, और कम से कम एक classification task को अधिक tokens खर्च करते हुए worse बनाती है। कौन सी कौन है, यह उन्हें पढ़कर obvious नहीं होता, और argument से settle नहीं होता।

इसलिए अगला chapter एक bench बनाता है: known answers वाले sixty cases, same prompt के चार variants, ठीक उसी client से parallel में run जिसे आपने अभी लिखा, Chapter 4 के confidence intervals के साथ tabulated — क्योंकि twenty cases पर four variants कुछ भी distinguish नहीं करते। एक sentence पूरे chapter को govern करता है: prompt measured होता है, debated नहीं।


ऊपर की हर संख्या mock provider से आई, Node 22 पर loopback interface के ऊपर, इसलिए latencies किसी real network से ज़्यादा clean हैं। यह deliberate है: जिन failures को measure किया जा रहा है उनमें से कोई भी network के कारण नहीं है, और ऐसा hostile server जिसे आप restart कर सकते हैं, real server से बेहतर सिखाता है जिसके लिए आपको pay करना पड़े और जिसे आप तोड़ न सकें।

  1. Server-Sent Events, WHATWG HTML Living Standard, section 9.2। Wire format — data: fields, blank-line-separated events, id: और retry: — वहीं define है, EventSource interface के साथ। EventSource request body या custom headers नहीं भेज सकता, इसलिए हर LLM client format को use करने के बजाय fetch के ऊपर हाथ से parse करता है।

  2. Brooker, M. Exponential Backoff and Jitter. AWS Architecture Blog (2015)। ऊपर use किए गए “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 की संख्या या HTTP date, दोनों accept करता है।

  4. Stripe, Idempotent requests, docs.stripe.com/api/idempotent_requests, 7 September 2026 को पढ़ा गया — contract का सबसे साफ statement: प्रति logical operation एक key, stored results replayed, first attempt अभी in flight होने पर conflict returned — और pattern provider-independent है। यहाँ use किए गए 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 library में wrap की गई उन्हीं concerns का best worked example है। सभी उसी दिन पढ़े गए।


निर्माता

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.
jev13 मिनट पढ़ें

Jev AI मॉडल गद्य के लिए नहीं, निर्णयों के लिए बना है

TypeSafe AI का Jev ध्यान खींच रहा है क्योंकि यह सॉफ्टवेयर इंटेलिजेंस को संभावना की समस्या मानता है: सही शाखा चुनें, भरोसे का स्तर जोड़ें, और जब कोड को निर्णय चाहिए तो LLM से टेक्स्ट लिखवाने पर खर्च न करें।

Abstract agent runtime sorting documents, memory blocks and pointer nodes inside a bounded context frame.
context-engineering14 मिनट पढ़ें

लॉन्ग-होराइजन AI एजेंट्स के लिए कॉन्टेक्स्ट इंजीनियरिंग

लंबे समय तक चलने वाले एजेंट सिर्फ इसलिए असफल नहीं होते कि विंडो छोटी है। वे तब असफल होते हैं जब फ़ाइलें, टूल आउटपुट और पुराना इतिहास उस काम को ही पीछे धकेल देते हैं जिसे एजेंट को पूरा करना था।

मॉडल चुनने का काम LIA पर छोड़ने के लिए तैयार हैं?

हर AI मॉडल एक ही जगह — आज ही मुफ़्त शुरू करें।