[{"data":1,"prerenderedAt":392},["ShallowReactive",2],{"tag-labels":3,"site-header":4,"article-your-stream-dies-at-125-seconds-en":72},{},{"featuredPost":5,"navigations":29},{"id":6,"title":7,"title_id":8,"slug":9,"excerpt":10,"excerpt_id":11,"has_translation":12,"published_at":13,"reading_time":14,"hidden_from_listing":15,"tags":16,"thumbnail_image":21,"og_image":22,"series":23,"authors":28},110,"When You Pick a Framework Just to Look Different","Ketika Pilih Framework Cuma Biar Kelihatan Beda","ketika-pilih-framework-cuma-biar-kelihatan-beda","The last time you chose a new framework or library, was it because your problem needed that tool, or because it sounded cool and few people around you were using it? That question is uncomfortable. Technology decisions are often made for reasons that never appear in any technical document.","Terakhir kali kamu milih framework atau library baru, itu karena masalahmu butuh alat itu, atau karena alatnya kedengaran keren dan belum banyak yang pakai? Pertanyaan ini nggak enak dijawab. Soalnya keputusan teknologi sering diambil dengan alasan yang nggak pernah ditulis di dokumen teknis mana pun.",true,"2026-10-06T15:25:21.000Z",5,false,[17,18,19,20],"karier","framework","opini","pengalaman",null,"\u002Fog\u002Fketika-pilih-framework-cuma-biar-kelihatan-beda.png",{"id":24,"title":25,"title_id":26,"slug":27},9,"Articles","Artikel","all",[],[30,46,67],{"collection":31,"item":32},"series",{"__typename":31,"title":25,"title_id":26,"slug":27,"children_series":33},[34,38,42],{"title":35,"title_id":36,"slug":37},"Artificial Intelligence","Kecerdasan Artifisial","kecerdasan-artifisial",{"title":39,"title_id":40,"slug":41},"Programming Fundamentals","Konsep Dasar Pemrograman","konsep-dasar-pemrograman",{"title":43,"title_id":44,"slug":45},"Algorithms and Data Structures","Algoritma dan Struktur Data","algoritma-struktur-data",{"collection":31,"item":47},{"__typename":31,"title":48,"title_id":48,"slug":49,"children_series":50},"Tutorial","tutorial",[51,55,59,63],{"title":52,"title_id":53,"slug":54},"Learning Python","Belajar Python","belajar-python",{"title":56,"title_id":57,"slug":58},"Learning Go","Belajar Golang","belajar-golang",{"title":60,"title_id":61,"slug":62},"Learning Java","Belajar Java","belajar-java",{"title":64,"title_id":65,"slug":66},"Learning Pascal","Belajar Pascal","belajar-pascal",{"collection":31,"item":68},{"__typename":31,"title":69,"title_id":69,"slug":70,"children_series":71},"Lab","lab",[],{"article":73,"renderedBody":293,"renderedTakeaway":294,"latestArticles":295,"toc":370},{"id":74,"title":75,"title_id":21,"slug":76,"excerpt":77,"excerpt_id":21,"has_translation":15,"published_at":78,"reading_time":79,"hidden_from_listing":15,"tags":80,"thumbnail_image":21,"og_image":85,"series":86,"authors":285,"body_locale":288,"available_locales":289,"body":291,"takeway":292,"prerequisite":21},134,"Your Stream Dies at 125 Seconds. No Timeout in Your Code Says 125.","your-stream-dies-at-125-seconds","Streaming does not remove the wait, it moves it into a connection that now outlives the request. Four layers sit between the model and the browser, their idle timeouts do not agree, and the smallest one ends your stream.","2026-11-04T01:00:00.000Z",7,[81,82,83,84],"streaming","sse","ai-agent","api","\u002Fog\u002Fyour-stream-dies-at-125-seconds.png",{"id":87,"title":35,"title_id":36,"slug":37,"description":88,"description_id":21,"articles":89,"children_series":277,"parent_series":278},8,"Notes on AI that go past the demo: how the models work, where they break, and what changes once you put them in production.",[90,107,125,140,154,168,184,200,221,240,259,264],{"id":91,"title":92,"title_id":93,"slug":94,"excerpt":95,"excerpt_id":96,"has_translation":12,"published_at":97,"reading_time":98,"hidden_from_listing":15,"tags":99,"thumbnail_image":21,"og_image":104,"series":105,"authors":106},1,"Why Does AI Keep Hallucinating? Meet JEPA, the AI Brain With Common Sense","Kenapa AI Suka 'Halu'? Kenalan sama JEPA, Otak AI yang Punya Akal Sehat","kenapa-ai-suka-halu-kenalan-sama-jepa-otak-ai-yang-punya-akal-sehat","Generative AI hallucinates because all it does is guess word patterns. JEPA tries to change that by teaching machines how the real world works, the way a human brain does.","AI generatif sering 'halu' karena cuma jago nebak pola kata. JEPA coba ngerombak itu dengan ngajarin AI cara kerja dunia nyata kayak otak manusia.","2026-06-02T08:09:05.595Z",4,[100,101,102,103],"jepa","artificial-intelligence","yann-lecun","agi","\u002Fog\u002Fkenapa-ai-suka-halu-kenalan-sama-jepa-otak-ai-yang-punya-akal-sehat.png",{"id":87,"title":35,"title_id":36,"slug":37},[],{"id":108,"title":109,"title_id":110,"slug":111,"excerpt":112,"excerpt_id":113,"has_translation":12,"published_at":114,"reading_time":98,"hidden_from_listing":15,"tags":115,"thumbnail_image":21,"og_image":117,"series":118,"authors":119},2,"LLMs Are Going to Hit a Wall. What Then?","LLM Bakal Mentok, Terus Gimana Dong?","llm-bakal-mentok-terus-gimana-dong","LLMs like ChatGPT are great at stringing words together, but they never really grasp reality. Yann LeCun says this architecture will hit a wall and needs a new answer.","LLM kayak ChatGPT itu jago ngerangkai kata, tapi nggak beneran paham realitas. Yann LeCun bilang arsitektur ini bakal mentok dan butuh solusi baru.","2026-06-03T08:55:33.003Z",[101,116,102,100],"llm","\u002Fog\u002Fllm-bakal-mentok-terus-gimana-dong.png",{"id":87,"title":35,"title_id":36,"slug":37},[120],{"authors_id":121},{"id":91,"name":122,"slug":123,"role":124,"profile_picture":21},"Daffa Izzuddin","izzudd","Writer",{"id":126,"title":127,"title_id":128,"slug":129,"excerpt":130,"excerpt_id":131,"has_translation":12,"published_at":132,"reading_time":98,"hidden_from_listing":15,"tags":133,"thumbnail_image":21,"og_image":135,"series":136,"authors":137},3,"Meet JEPA: Where AI Starts Learning Common Sense","Mengenal JEPA: Saat AI Mulai Belajar Punya \"Akal Sehat\"","kenapa-ai-suka-halu-dan-gimana-jepa-beresin-masalah-ini","AI hallucinates because it only guesses data probabilities without understanding reality. The JEPA architecture tries to change that by building a world model in its head.","AI sering 'halu' karena cuma nebak probabilitas data tanpa paham realita. Arsitektur JEPA mencoba mengubah itu dengan membangun model dunia di kepalanya.","2026-06-03T09:32:50.446Z",[101,100,134,102],"world-models","\u002Fog\u002Fkenapa-ai-suka-halu-dan-gimana-jepa-beresin-masalah-ini.png",{"id":87,"title":35,"title_id":36,"slug":37},[138],{"authors_id":139},{"id":91,"name":122,"slug":123,"role":124,"profile_picture":21},{"id":98,"title":141,"title_id":142,"slug":143,"excerpt":144,"excerpt_id":145,"has_translation":12,"published_at":146,"reading_time":98,"hidden_from_listing":15,"tags":147,"thumbnail_image":21,"og_image":149,"series":150,"authors":151},"Beyond Generative AI: JEPA Chases Human-Style Conceptual Understanding","Bukan Sekadar Generative AI, JEPA Coba Kejar Pemahaman Konsep ala Manusia","kenapa-ai-masih-gagal-paham-fisika-dasar-mengenal-arsitektur-jepa","AI usually only guesses pixels, while JEPA tries to learn from abstract concepts in latent space. That approach turns out to reach as far as reading brain waves.","AI biasanya cuma jago nebak piksel, tapi JEPA coba belajar lewat konsep abstrak di ruang laten. Ternyata, cara ini bisa sampai dipakai buat baca gelombang otak.","2026-06-03T09:34:14.840Z",[100,102,148,103],"machine-learning","\u002Fog\u002Fkenapa-ai-masih-gagal-paham-fisika-dasar-mengenal-arsitektur-jepa.png",{"id":87,"title":35,"title_id":36,"slug":37},[152],{"authors_id":153},{"id":91,"name":122,"slug":123,"role":124,"profile_picture":21},{"id":14,"title":155,"title_id":156,"slug":157,"excerpt":158,"excerpt_id":159,"has_translation":12,"published_at":160,"reading_time":98,"hidden_from_listing":15,"tags":161,"thumbnail_image":21,"og_image":163,"series":164,"authors":165},"Inside JEPA's Kitchen: The Secrets of EMA, VICReg, and Compute Efficiency","Di Balik Dapur JEPA: Rahasia EMA, VICReg, dan Efisiensi Komputasi","di-balik-dapur-jepa-rahasia-ema-vicreg-dan-efisiensi-komputasi","Bored of AI that only guesses words or memorizes pixels? Meet JEPA, an architecture that teaches machines common sense very cheaply. Here is a look at its technical insides.","Bosen sama AI yang cuma jago nebak kata atau ngafalin pixel? Kenalan sama JEPA, arsitektur revolusioner yang ngajarin mesin punya \"akal sehat\" secara super efisien. Yuk intip jeroan teknisnya!","2026-06-07T16:14:13.723Z",[100,162],"artificial-intelligent","\u002Fog\u002Fdi-balik-dapur-jepa-rahasia-ema-vicreg-dan-efisiensi-komputasi.png",{"id":87,"title":35,"title_id":36,"slug":37},[166],{"authors_id":167},{"id":91,"name":122,"slug":123,"role":124,"profile_picture":21},{"id":169,"title":170,"title_id":171,"slug":172,"excerpt":173,"excerpt_id":174,"has_translation":12,"published_at":175,"reading_time":87,"hidden_from_listing":15,"tags":176,"thumbnail_image":21,"og_image":181,"series":182,"authors":183},125,"MCP, Rebuilt: Good News If You're Tired of Writing Connectors","MCP dibongkar ulang: kabar baik buat kamu yang capek nulis konektor","mcp-dibongkar-ulang-kabar-baik-buat-kamu","You have a few internal APIs and one AI agent, but the code has to be rewritten every time the model changes. MCP ends that, and this year the protocol was rebuilt so servers can run like ordinary web servers. Here's what changed. 🔧","Kamu punya beberapa API internal dan satu agent AI, tapi kodenya harus ditulis ulang tiap kali modelnya diganti. MCP datang buat mengakhiri itu, dan tahun ini protokolnya dibongkar ulang supaya server-nya bisa jalan seperti web server biasa. Yuk lihat apa yang berubah. 🔧","2026-10-16T05:00:00.000Z",[177,178,83,116,179,180],"mcp","model-context-protocol","backend","python","\u002Fog\u002Fmcp-dibongkar-ulang-kabar-baik-buat-kamu.png",{"id":87,"title":35,"title_id":36,"slug":37},[],{"id":185,"title":186,"title_id":187,"slug":188,"excerpt":189,"excerpt_id":190,"has_translation":12,"published_at":191,"reading_time":79,"hidden_from_listing":15,"tags":192,"thumbnail_image":21,"og_image":195,"series":196,"authors":197},126,"AI Agent Memory: From Vector Store to Temporal Graph","Memori AI Agent: dari Vector Store ke Temporal Graph","memori-ai-agent-vector-store-ke-temporal-graph","Two contradictory facts make an agent answer nonsense. Here is how a vector store and a temporal knowledge graph differ at keeping long-term state.","Dua fakta yang saling bertentangan bikin agent menjawab ngawur. Ini bedanya vector store dan temporal knowledge graph dalam menjaga state jangka panjang.","2026-10-18T13:13:29.458Z",[193,116,179,194],"ai","system-design","\u002Fog\u002Fmemori-ai-agent-vector-store-ke-temporal-graph.png",{"id":87,"title":35,"title_id":36,"slug":37},[198],{"authors_id":199},{"id":91,"name":122,"slug":123,"role":124,"profile_picture":21},{"id":201,"title":202,"title_id":203,"slug":204,"excerpt":205,"excerpt_id":206,"has_translation":12,"published_at":207,"reading_time":208,"hidden_from_listing":15,"tags":209,"thumbnail_image":21,"og_image":216,"series":217,"authors":218},127,"Why RAG That Leans on Cosine Similarity Alone Keeps Missing","Kenapa RAG yang cuma andalkan cosine similarity sering nyasar","kenapa-rag-cuma-cosine-similarity-sering-nyasar","RAG that leans only on embeddings returns chunks that look close in meaning, not the ones you need. Here is why, and the two layers that patch it.","RAG yang cuma andalkan embedding balikin chunk yang mirip maknanya, bukan yang kamu butuh. Ini penyebabnya, dan dua lapis penambalnya.","2026-10-19T02:04:06.718Z",6,[210,211,212,213,214,215],"rag","vector-search","bm25","hybrid-search","retrieval","embedding","\u002Fog\u002Fkenapa-rag-cuma-cosine-similarity-sering-nyasar.png",{"id":87,"title":35,"title_id":36,"slug":37},[219],{"authors_id":220},{"id":91,"name":122,"slug":123,"role":124,"profile_picture":21},{"id":222,"title":223,"title_id":224,"slug":225,"excerpt":226,"excerpt_id":227,"has_translation":12,"published_at":228,"reading_time":208,"hidden_from_listing":15,"tags":229,"thumbnail_image":21,"og_image":235,"series":236,"authors":237},128,"Linear or Graph: How to Run Many Agents Without an Eternal Loop","Linear atau graf: cara mengatur banyak agent tanpa loop abadi","multi-agen-linear-atau-grafik","One agent with twenty tools gets messy fast. Once several agents revise each other, you need a graph that allows cycles and a number that makes it stop.","Satu agent dengan dua puluh tool gampang kacau. Begitu beberapa agent saling revisi, kamu butuh graf yang mendukung cycle dan angka yang membuatnya berhenti.","2026-10-21T16:05:00.000Z",[230,231,232,233,234,116],"multi-agent","langgraph","autogen","orkestrasi","checkpoint","\u002Fog\u002Fmulti-agen-linear-atau-grafik.png",{"id":87,"title":35,"title_id":36,"slug":37},[238],{"authors_id":239},{"id":91,"name":122,"slug":123,"role":124,"profile_picture":21},{"id":241,"title":242,"title_id":243,"slug":244,"excerpt":245,"excerpt_id":246,"has_translation":12,"published_at":247,"reading_time":14,"hidden_from_listing":15,"tags":248,"thumbnail_image":21,"og_image":254,"series":255,"authors":256},129,"So Your RAG Stops Hitting 504: Split the Query Path From Ingestion","Biar RAG nggak kena 504: pisahin jalur query dan ingestion","rag-pisah-jalur-query-dan-ingestion","One RAG service that both parses uploads and answers queries will fall over the moment someone uploads a 200-page PDF. The cure is two paths and one queue.","Satu service RAG yang merangkap parsing dan menjawab kueri bakal tumbang pas ada yang upload PDF 200 halaman. Obatnya dua jalur dan satu queue.","2026-10-22T01:30:00.000Z",[210,249,250,251,252,253],"cloud-run","cloud-tasks","gcp","serverless","arsitektur","\u002Fog\u002Frag-pisah-jalur-query-dan-ingestion.png",{"id":87,"title":35,"title_id":36,"slug":37},[257],{"authors_id":258},{"id":91,"name":122,"slug":123,"role":124,"profile_picture":21},{"id":74,"title":75,"title_id":21,"slug":76,"excerpt":77,"excerpt_id":21,"has_translation":15,"published_at":78,"reading_time":79,"hidden_from_listing":15,"tags":80,"thumbnail_image":21,"og_image":85,"series":260,"authors":261},{"id":87,"title":35,"title_id":36,"slug":37},[262],{"authors_id":263},{"id":91,"name":122,"slug":123,"role":124,"profile_picture":21},{"id":265,"title":266,"title_id":21,"slug":267,"excerpt":268,"excerpt_id":21,"has_translation":15,"published_at":269,"reading_time":79,"hidden_from_listing":15,"tags":270,"thumbnail_image":21,"og_image":272,"series":273,"authors":274},135,"Caching That Never Hits Costs More Than Not Caching at All","caching-that-never-hits","Same model, same conversation, same token count, and a bill that is five times higher. The difference is a prefix that changes, a write premium nobody reads about, and a cost curve that grows faster than the loop.","2026-11-04T01:10:00.000Z",[116,83,271,84],"cache","\u002Fog\u002Fcaching-that-never-hits.png",{"id":87,"title":35,"title_id":36,"slug":37},[275],{"authors_id":276},{"id":91,"name":122,"slug":123,"role":124,"profile_picture":21},[],{"id":24,"title":25,"title_id":26,"slug":27,"parent_series":21,"children_series":279},[280,281,283],{"id":87,"title":35,"title_id":36,"slug":37,"cover_image":21},{"id":282,"title":39,"title_id":40,"slug":41,"cover_image":21},12,{"id":284,"title":43,"title_id":44,"slug":45,"cover_image":21},15,[286],{"authors_id":287},{"id":91,"name":122,"slug":123,"role":124,"profile_picture":21},"en",[288,290],"id","\nThe text stops mid-sentence. The client shows a generic network error, the handler logs no exception, and the same endpoint that streamed fine on localhost dies in production after a suspiciously round interval.\n\nNothing in the code threw. Something else closed the connection, and that something else is a default nobody set.\n\nStreaming is usually described as a user experience feature: show tokens as they arrive so the wait feels shorter. That description is true and it is also the reason the failure is hard to find. The wait did not disappear. It moved into a connection that now has to stay open while the work finishes, and every layer between your model and your browser has its own opinion about how long an open connection may stay quiet.\n\n## Streaming moves the wait, it does not remove it\n\nTime to first token is the time between sending a request and receiving the first token of the response. For reasoning models, the first token counted is the first reasoning token, not the first visible word ([Artificial Analysis](https:\u002F\u002Fartificialanalysis.ai\u002Fmethodology\u002Fperformance-benchmarking)). That is the number streaming improves.\n\nThe total time does not change. The model still needs the same seconds to finish, and the connection exists for all of them. Between the first token and the last, the stream is mostly silence with something written into it every so often, and how often is decided by the model, not by you.\n\nThat distinction is the whole article. If your provider sends events every 40 milliseconds, an idle timeout of 60 seconds is generous and irrelevant. If your agent loop spends 90 seconds inside a tool call before the next token, that same 60 seconds is the thing that ends the run.\n\n## Four layers, six timeouts, no two agree\n\nHere are the defaults that sit between a model call and a browser, read from the primary documentation on 2026-11-03.\n\n| Layer | Setting | Default | What it does |\n|---|---|---|---|\n| nginx | `proxy_read_timeout` | 60s | Closes the upstream connection if nothing arrives between two reads |\n| nginx | `proxy_buffering` | on | Holds the response in buffers, so early events can arrive at the client late or in a burst |\n| Cloudflare | Proxy Read Timeout | 125s | Returns 524 if the origin sends no response in that window |\n| Cloudflare | Proxy Write Timeout | 30s | Returns 524 if the write to the origin does not finish in time |\n| Node | `server.requestTimeout` | 300s | Answers 408 if the whole request is not received in time |\n| Node | `server.timeout` | 0 | No socket inactivity timeout at all |\n| Go | `Server.WriteTimeout` | 0 | No write timeout at all |\n\nThe numbers come from the [nginx proxy module documentation](https:\u002F\u002Fnginx.org\u002Fen\u002Fdocs\u002Fhttp\u002Fngx_http_proxy_module.html), [Cloudflare's connection limits reference](https:\u002F\u002Fdevelopers.cloudflare.com\u002Ffundamentals\u002Freference\u002Fconnection-limits\u002F), the [Node HTTP documentation](https:\u002F\u002Fnodejs.org\u002Fapi\u002Fhttp.html), and the [Go `net\u002Fhttp` documentation](https:\u002F\u002Fpkg.go.dev\u002Fnet\u002Fhttp).\n\nRead the table as a list of clocks, not settings. Six of them, in four layers, and the difference between the smallest and the largest is not a percentage. Whichever fires first ends your stream, and that is usually not the one you configured, because the one you configured is the one you thought about.\n\nTwo details in that table deserve more than a row.\n\nThe first is nginx. `proxy_read_timeout` is set \"only between two successive read operations, not for the transmission of the whole response\" ([nginx docs](https:\u002F\u002Fnginx.org\u002Fen\u002Fdocs\u002Fhttp\u002Fngx_http_proxy_module.html)). A stream that runs for three minutes and emits a token every five seconds will outlive a 60 second timeout indefinitely. A stream that runs for 90 seconds and thinks for 61 of them does not. Duration is not the risk. The longest silence is.\n\nThe second is Cloudflare, because 125 is not the number most people repeat. The widely quoted figure is 100 seconds, from older write-ups; the current reference says 125 for the proxy read timeout and 30 for the write timeout, and only the read timeout is adjustable, and only on Enterprise zones, up to 6,000 seconds ([error 524](https:\u002F\u002Fdevelopers.cloudflare.com\u002Fsupport\u002Ftroubleshooting\u002Fhttp-status-codes\u002Fcloudflare-5xx-errors\u002Ferror-524)). If the number in your head came from a blog post, it is worth checking against the table before building an argument on it.\n\n## This is not a WebSocket problem\n\nThe first instinct when a stream dies is to reach for a socket, on the theory that a WebSocket is a more durable kind of connection. It is not. It is a connection with a different lifecycle, and it inherits the same timers, the same load balancer, and the same deploy that drains a node.\n\nWhat a WebSocket actually buys is a channel back to the server while the response is open. That matters when the reader has to interrupt generation, steer it, or talk at the same time. Token streaming does not need it, because token streaming goes one way.\n\nServer-sent events are the cheaper tool and they come with two things people rebuild badly by hand. An `EventSource` reconnects on its own after the connection drops ([MDN](https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FEventSource)), and the spec defines a `Last-Event-ID` header that the browser sends on that reconnect, carrying the id of the last event it received ([HTML spec](https:\u002F\u002Fhtml.spec.whatwg.org\u002Fmultipage\u002Fserver-sent-events.html#the-last-event-id-header)). Number your events and resumption is a lookup, not a feature.\n\nThe reconnect delay is implementation-defined, \"probably in the region of a few seconds\" in the spec's own words, and the server can override it with the `retry` field. That is the knob for a stream that gets killed by a proxy: if your client waits 3 seconds before asking again and your run is four minutes long, you get a reconnect every time the proxy idles, forever.\n\nOne more number worth knowing before designing around open streams: over HTTP\u002F2 the number of simultaneous streams is negotiated between client and server and defaults to 100 ([MDN](https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FEventSource)). Every open SSE stream is one of those, which is fine until one page opens a stream per widget.\n\n## When the run takes four minutes, the stream is not the transport\n\nHere the shape changes, not just the size.\n\nA four minute agent loop is not a request that happens to be slow. It is a job: it can be retried, it can outlive a browser tab, and it can be cancelled halfway. The moment that is true, the response stops being the record of what happened. It becomes a view of a record that has to exist somewhere else.\n\nCloudflare's own 524 page recommends status polling for long running HTTP processes. That is the vendor telling you the same thing: do not make the connection carry the truth.\n\nThe design that survives is boring. Accept the job, return an id, append events to a log with a sequence number as they are produced, and let the stream read from that log. A reconnect with `Last-Event-ID` becomes a request for \"everything after 412\" rather than a request to run the work again. The client can close the tab, and the run continues, because the run never depended on the tab.\n\nYou cannot resume what you did not write down. That sentence is the whole section.\n\n## Cancelling is a decision on the server\n\nClosing the socket is a client action. It says nothing to the model call that is still generating tokens into a buffer nobody will read.\n\nUnless the abort is propagated, the stop button stops the pixels, not the bill. In a streaming handler this is a few lines: listen for the close event, cancel the request to the provider with the same signal, and let the cancellation travel the whole way down. In a job-shaped design it is a message to the worker, plus a rule for what a cancelled job leaves behind.\n\nThis is testable without a benchmark. Run the probe below with an abort time, then compare the token usage your provider reports for that request against a run you let finish.\n\n## Measure your own number\n\nThe table above is a list of other people's defaults. The number that matters is the longest silence your stream can produce, and that is a property of your loop, not of nginx.\n\nSave this as `sse-probe.mjs` and point it at your endpoint. It reports time to first token, total time, the largest gap between two chunks, and how many event boundaries arrived.\n\n```js\n\u002F\u002F sse-probe.mjs — node sse-probe.mjs \u003Curl> [abortAfterMs]\nconst url = process.argv[2];\nconst abortAfter = Number(process.argv[3] || 0);\nconst start = performance.now();\nconst ac = new AbortController();\nif (abortAfter) setTimeout(() => ac.abort(), abortAfter);\n\nlet first = null, last = null, gap = 0, events = 0, bytes = 0;\ntry {\n  const res = await fetch(url, { signal: ac.signal });\n  const reader = res.body.getReader();\n  const dec = new TextDecoder();\n  let buf = \"\";\n  for (;;) {\n    const { value, done } = await reader.read();\n    if (done) break;\n    const now = performance.now();\n    bytes += value.byteLength;\n    if (first === null) first = now;\n    if (last !== null) gap = Math.max(gap, now - last);\n    last = now;\n    buf += dec.decode(value, { stream: true });\n    events = (buf.match(\u002F\\n\\n\u002Fg) || []).length;\n  }\n} catch (err) {\n  console.log(`aborted at ${Math.round(performance.now() - start)}ms (${err.name})`);\n}\nconsole.log({\n  ttftMs: first && Math.round(first - start),\n  totalMs: Math.round((last || start) - start),\n  largestGapMs: Math.round(gap),\n  events,\n  bytes,\n});\n```\n\nCompare `largestGapMs` against the smallest timeout in your stack, not against `totalMs`. If the gap loses, the stream will die in production on exactly the runs where the model thinks hardest, which is the opposite of the runs you want to lose.\n\n## What this does not cover\n\nThe numbers here are documentation defaults read on 2026-11-03, not measurements taken on your stack. Your reverse proxy may already override them, your platform may add another layer, and an Enterprise zone can move the Cloudflare timeout to a value this table does not describe.\n\nIt also does not cover HTTP\u002F3 behaviour, gRPC streaming, or provider-side caps on how long a single response may stay open. Those are real and they behave differently; they are a second article.\n\nIf your p95 run duration is longer than the shortest timeout in your stack, the stream is a view over a log, not the transport of truth. Build the log first.\n","- Streaming removes the wait before the first token, not the work. The connection lives for the whole gap between the first token and the last.\n- Four layers sit between the model and the browser and their idle timeouts do not agree: nginx reads 60 seconds, Cloudflare 125, Node 300, and Go has none by default.\n- nginx times out between two successive reads, not over the whole response, so the longest silence in your stream is the number that kills it, not the total duration.\n- SSE already gives reconnection and a Last-Event-ID header. A WebSocket adds a channel back to the server and loses both of them.\n- When a run takes minutes, persist the events first and stream them second, because a reconnect has nothing to replay if the events only ever existed inside one response.","\u003Cp>The text stops mid-sentence. The client shows a generic network error, the handler logs no exception, and the same endpoint that streamed fine on localhost dies in production after a suspiciously round interval.\u003C\u002Fp>\n\u003Cp>Nothing in the code threw. Something else closed the connection, and that something else is a default nobody set.\u003C\u002Fp>\n\u003Cp>Streaming is usually described as a user experience feature: show tokens as they arrive so the wait feels shorter. That description is true and it is also the reason the failure is hard to find. The wait did not disappear. It moved into a connection that now has to stay open while the work finishes, and every layer between your model and your browser has its own opinion about how long an open connection may stay quiet.\u003C\u002Fp>\n\u003Ch2 id=\"streaming-moves-the-wait-it-does-not-remove-it\">Streaming moves the wait, it does not remove it\u003C\u002Fh2>\n\u003Cp>Time to first token is the time between sending a request and receiving the first token of the response. For reasoning models, the first token counted is the first reasoning token, not the first visible word (\u003Ca href=\"https:\u002F\u002Fartificialanalysis.ai\u002Fmethodology\u002Fperformance-benchmarking\">Artificial Analysis\u003C\u002Fa>). That is the number streaming improves.\u003C\u002Fp>\n\u003Cp>The total time does not change. The model still needs the same seconds to finish, and the connection exists for all of them. Between the first token and the last, the stream is mostly silence with something written into it every so often, and how often is decided by the model, not by you.\u003C\u002Fp>\n\u003Cp>That distinction is the whole article. If your provider sends events every 40 milliseconds, an idle timeout of 60 seconds is generous and irrelevant. If your agent loop spends 90 seconds inside a tool call before the next token, that same 60 seconds is the thing that ends the run.\u003C\u002Fp>\n\u003Ch2 id=\"four-layers-six-timeouts-no-two-agree\">Four layers, six timeouts, no two agree\u003C\u002Fh2>\n\u003Cp>Here are the defaults that sit between a model call and a browser, read from the primary documentation on 2026-11-03.\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Layer\u003C\u002Fth>\n\u003Cth>Setting\u003C\u002Fth>\n\u003Cth>Default\u003C\u002Fth>\n\u003Cth>What it does\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>nginx\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>proxy_read_timeout\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>60s\u003C\u002Ftd>\n\u003Ctd>Closes the upstream connection if nothing arrives between two reads\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>nginx\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>proxy_buffering\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>on\u003C\u002Ftd>\n\u003Ctd>Holds the response in buffers, so early events can arrive at the client late or in a burst\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Cloudflare\u003C\u002Ftd>\n\u003Ctd>Proxy Read Timeout\u003C\u002Ftd>\n\u003Ctd>125s\u003C\u002Ftd>\n\u003Ctd>Returns 524 if the origin sends no response in that window\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Cloudflare\u003C\u002Ftd>\n\u003Ctd>Proxy Write Timeout\u003C\u002Ftd>\n\u003Ctd>30s\u003C\u002Ftd>\n\u003Ctd>Returns 524 if the write to the origin does not finish in time\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Node\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>server.requestTimeout\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>300s\u003C\u002Ftd>\n\u003Ctd>Answers 408 if the whole request is not received in time\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Node\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>server.timeout\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>0\u003C\u002Ftd>\n\u003Ctd>No socket inactivity timeout at all\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Go\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>Server.WriteTimeout\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>0\u003C\u002Ftd>\n\u003Ctd>No write timeout at all\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Cp>The numbers come from the \u003Ca href=\"https:\u002F\u002Fnginx.org\u002Fen\u002Fdocs\u002Fhttp\u002Fngx_http_proxy_module.html\">nginx proxy module documentation\u003C\u002Fa>, \u003Ca href=\"https:\u002F\u002Fdevelopers.cloudflare.com\u002Ffundamentals\u002Freference\u002Fconnection-limits\u002F\">Cloudflare&#39;s connection limits reference\u003C\u002Fa>, the \u003Ca href=\"https:\u002F\u002Fnodejs.org\u002Fapi\u002Fhttp.html\">Node HTTP documentation\u003C\u002Fa>, and the \u003Ca href=\"https:\u002F\u002Fpkg.go.dev\u002Fnet\u002Fhttp\">Go \u003Ccode>net\u002Fhttp\u003C\u002Fcode> documentation\u003C\u002Fa>.\u003C\u002Fp>\n\u003Cp>Read the table as a list of clocks, not settings. Six of them, in four layers, and the difference between the smallest and the largest is not a percentage. Whichever fires first ends your stream, and that is usually not the one you configured, because the one you configured is the one you thought about.\u003C\u002Fp>\n\u003Cp>Two details in that table deserve more than a row.\u003C\u002Fp>\n\u003Cp>The first is nginx. \u003Ccode>proxy_read_timeout\u003C\u002Fcode> is set &quot;only between two successive read operations, not for the transmission of the whole response&quot; (\u003Ca href=\"https:\u002F\u002Fnginx.org\u002Fen\u002Fdocs\u002Fhttp\u002Fngx_http_proxy_module.html\">nginx docs\u003C\u002Fa>). A stream that runs for three minutes and emits a token every five seconds will outlive a 60 second timeout indefinitely. A stream that runs for 90 seconds and thinks for 61 of them does not. Duration is not the risk. The longest silence is.\u003C\u002Fp>\n\u003Cp>The second is Cloudflare, because 125 is not the number most people repeat. The widely quoted figure is 100 seconds, from older write-ups; the current reference says 125 for the proxy read timeout and 30 for the write timeout, and only the read timeout is adjustable, and only on Enterprise zones, up to 6,000 seconds (\u003Ca href=\"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fsupport\u002Ftroubleshooting\u002Fhttp-status-codes\u002Fcloudflare-5xx-errors\u002Ferror-524\">error 524\u003C\u002Fa>). If the number in your head came from a blog post, it is worth checking against the table before building an argument on it.\u003C\u002Fp>\n\u003Ch2 id=\"this-is-not-a-websocket-problem\">This is not a WebSocket problem\u003C\u002Fh2>\n\u003Cp>The first instinct when a stream dies is to reach for a socket, on the theory that a WebSocket is a more durable kind of connection. It is not. It is a connection with a different lifecycle, and it inherits the same timers, the same load balancer, and the same deploy that drains a node.\u003C\u002Fp>\n\u003Cp>What a WebSocket actually buys is a channel back to the server while the response is open. That matters when the reader has to interrupt generation, steer it, or talk at the same time. Token streaming does not need it, because token streaming goes one way.\u003C\u002Fp>\n\u003Cp>Server-sent events are the cheaper tool and they come with two things people rebuild badly by hand. An \u003Ccode>EventSource\u003C\u002Fcode> reconnects on its own after the connection drops (\u003Ca href=\"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FEventSource\">MDN\u003C\u002Fa>), and the spec defines a \u003Ccode>Last-Event-ID\u003C\u002Fcode> header that the browser sends on that reconnect, carrying the id of the last event it received (\u003Ca href=\"https:\u002F\u002Fhtml.spec.whatwg.org\u002Fmultipage\u002Fserver-sent-events.html#the-last-event-id-header\">HTML spec\u003C\u002Fa>). Number your events and resumption is a lookup, not a feature.\u003C\u002Fp>\n\u003Cp>The reconnect delay is implementation-defined, &quot;probably in the region of a few seconds&quot; in the spec&#39;s own words, and the server can override it with the \u003Ccode>retry\u003C\u002Fcode> field. That is the knob for a stream that gets killed by a proxy: if your client waits 3 seconds before asking again and your run is four minutes long, you get a reconnect every time the proxy idles, forever.\u003C\u002Fp>\n\u003Cp>One more number worth knowing before designing around open streams: over HTTP\u002F2 the number of simultaneous streams is negotiated between client and server and defaults to 100 (\u003Ca href=\"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FEventSource\">MDN\u003C\u002Fa>). Every open SSE stream is one of those, which is fine until one page opens a stream per widget.\u003C\u002Fp>\n\u003Ch2 id=\"when-the-run-takes-four-minutes-the-stream-is-not-the-transport\">When the run takes four minutes, the stream is not the transport\u003C\u002Fh2>\n\u003Cp>Here the shape changes, not just the size.\u003C\u002Fp>\n\u003Cp>A four minute agent loop is not a request that happens to be slow. It is a job: it can be retried, it can outlive a browser tab, and it can be cancelled halfway. The moment that is true, the response stops being the record of what happened. It becomes a view of a record that has to exist somewhere else.\u003C\u002Fp>\n\u003Cp>Cloudflare&#39;s own 524 page recommends status polling for long running HTTP processes. That is the vendor telling you the same thing: do not make the connection carry the truth.\u003C\u002Fp>\n\u003Cp>The design that survives is boring. Accept the job, return an id, append events to a log with a sequence number as they are produced, and let the stream read from that log. A reconnect with \u003Ccode>Last-Event-ID\u003C\u002Fcode> becomes a request for &quot;everything after 412&quot; rather than a request to run the work again. The client can close the tab, and the run continues, because the run never depended on the tab.\u003C\u002Fp>\n\u003Cp>You cannot resume what you did not write down. That sentence is the whole section.\u003C\u002Fp>\n\u003Ch2 id=\"cancelling-is-a-decision-on-the-server\">Cancelling is a decision on the server\u003C\u002Fh2>\n\u003Cp>Closing the socket is a client action. It says nothing to the model call that is still generating tokens into a buffer nobody will read.\u003C\u002Fp>\n\u003Cp>Unless the abort is propagated, the stop button stops the pixels, not the bill. In a streaming handler this is a few lines: listen for the close event, cancel the request to the provider with the same signal, and let the cancellation travel the whole way down. In a job-shaped design it is a message to the worker, plus a rule for what a cancelled job leaves behind.\u003C\u002Fp>\n\u003Cp>This is testable without a benchmark. Run the probe below with an abort time, then compare the token usage your provider reports for that request against a run you let finish.\u003C\u002Fp>\n\u003Ch2 id=\"measure-your-own-number\">Measure your own number\u003C\u002Fh2>\n\u003Cp>The table above is a list of other people&#39;s defaults. The number that matters is the longest silence your stream can produce, and that is a property of your loop, not of nginx.\u003C\u002Fp>\n\u003Cp>Save this as \u003Ccode>sse-probe.mjs\u003C\u002Fcode> and point it at your endpoint. It reports time to first token, total time, the largest gap between two chunks, and how many event boundaries arrived.\u003C\u002Fp>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">JS\u003C\u002Fspan>\n    \u003C\u002Fdiv>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"%2F%2F%20sse-probe.mjs%20%E2%80%94%20node%20sse-probe.mjs%20%3Curl%3E%20%5BabortAfterMs%5D%0Aconst%20url%20%3D%20process.argv%5B2%5D%3B%0Aconst%20abortAfter%20%3D%20Number(process.argv%5B3%5D%20%7C%7C%200)%3B%0Aconst%20start%20%3D%20performance.now()%3B%0Aconst%20ac%20%3D%20new%20AbortController()%3B%0Aif%20(abortAfter)%20setTimeout(()%20%3D%3E%20ac.abort()%2C%20abortAfter)%3B%0A%0Alet%20first%20%3D%20null%2C%20last%20%3D%20null%2C%20gap%20%3D%200%2C%20events%20%3D%200%2C%20bytes%20%3D%200%3B%0Atry%20%7B%0A%20%20const%20res%20%3D%20await%20fetch(url%2C%20%7B%20signal%3A%20ac.signal%20%7D)%3B%0A%20%20const%20reader%20%3D%20res.body.getReader()%3B%0A%20%20const%20dec%20%3D%20new%20TextDecoder()%3B%0A%20%20let%20buf%20%3D%20%22%22%3B%0A%20%20for%20(%3B%3B)%20%7B%0A%20%20%20%20const%20%7B%20value%2C%20done%20%7D%20%3D%20await%20reader.read()%3B%0A%20%20%20%20if%20(done)%20break%3B%0A%20%20%20%20const%20now%20%3D%20performance.now()%3B%0A%20%20%20%20bytes%20%2B%3D%20value.byteLength%3B%0A%20%20%20%20if%20(first%20%3D%3D%3D%20null)%20first%20%3D%20now%3B%0A%20%20%20%20if%20(last%20!%3D%3D%20null)%20gap%20%3D%20Math.max(gap%2C%20now%20-%20last)%3B%0A%20%20%20%20last%20%3D%20now%3B%0A%20%20%20%20buf%20%2B%3D%20dec.decode(value%2C%20%7B%20stream%3A%20true%20%7D)%3B%0A%20%20%20%20events%20%3D%20(buf.match(%2F%5Cn%5Cn%2Fg)%20%7C%7C%20%5B%5D).length%3B%0A%20%20%7D%0A%7D%20catch%20(err)%20%7B%0A%20%20console.log(%60aborted%20at%20%24%7BMath.round(performance.now()%20-%20start)%7Dms%20(%24%7Berr.name%7D)%60)%3B%0A%7D%0Aconsole.log(%7B%0A%20%20ttftMs%3A%20first%20%26%26%20Math.round(first%20-%20start)%2C%0A%20%20totalMs%3A%20Math.round((last%20%7C%7C%20start)%20-%20start)%2C%0A%20%20largestGapMs%3A%20Math.round(gap)%2C%0A%20%20events%2C%0A%20%20bytes%2C%0A%7D)%3B\" title=\"Salin kode\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">\u002F\u002F sse-probe.mjs — node sse-probe.mjs &#x3C;url> [abortAfterMs]\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">const\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> url\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> process.argv[\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">2\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">];\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">const\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> abortAfter\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\"> Number\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(process.argv[\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">3\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">] \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">||\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> 0\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">const\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> start\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> performance.\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\">now\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">const\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> ac\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\"> AbortController\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">if\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> (abortAfter) \u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\">setTimeout\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(() \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=>\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> ac.\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\">abort\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(), abortAfter);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">let\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> first \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> null\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, last \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> null\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, gap \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> 0\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, events \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> 0\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, bytes \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> 0\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">try\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> res\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> await\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\"> fetch\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(url, { signal: ac.signal });\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> reader\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> res.body.\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\">getReader\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> dec\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\"> TextDecoder\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  let\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> buf \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> \"\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> (;;) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    const\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> { \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">value\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">done\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> } \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> await\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> reader.\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\">read\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    if\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> (done) \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">break\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    const\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> now\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> performance.\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\">now\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    bytes \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">+=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> value.byteLength;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    if\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> (first \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">===\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> null\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">) first \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> now;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    if\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> (last \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">!==\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> null\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">) gap \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> Math.\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\">max\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(gap, now \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">-\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> last);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    last \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> now;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    buf \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">+=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> dec.\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\">decode\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(value, { stream: \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">true\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> });\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    events \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> (buf.\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\">match\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\u002F\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">\\n\\n\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\u002F\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">g\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">) \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">||\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> []).\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">length\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">} \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">catch\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> (err) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  console.\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\">log\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">`aborted at ${\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">Math\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">.\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\">round\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">(\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">performance\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">.\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\">now\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">() \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">-\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> start\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">)\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">}ms (${\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">err\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">.\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">name\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">})`\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">console.\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\">log\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">({\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  ttftMs: first \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">&#x26;&#x26;\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> Math.\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\">round\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(first \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">-\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> start),\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  totalMs: Math.\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\">round\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">((last \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">||\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> start) \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">-\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> start),\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  largestGapMs: Math.\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\">round\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(gap),\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  events,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  bytes,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">});\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>Compare \u003Ccode>largestGapMs\u003C\u002Fcode> against the smallest timeout in your stack, not against \u003Ccode>totalMs\u003C\u002Fcode>. If the gap loses, the stream will die in production on exactly the runs where the model thinks hardest, which is the opposite of the runs you want to lose.\u003C\u002Fp>\n\u003Ch2 id=\"what-this-does-not-cover\">What this does not cover\u003C\u002Fh2>\n\u003Cp>The numbers here are documentation defaults read on 2026-11-03, not measurements taken on your stack. Your reverse proxy may already override them, your platform may add another layer, and an Enterprise zone can move the Cloudflare timeout to a value this table does not describe.\u003C\u002Fp>\n\u003Cp>It also does not cover HTTP\u002F3 behaviour, gRPC streaming, or provider-side caps on how long a single response may stay open. Those are real and they behave differently; they are a second article.\u003C\u002Fp>\n\u003Cp>If your p95 run duration is longer than the shortest timeout in your stack, the stream is a view over a log, not the transport of truth. Build the log first.\u003C\u002Fp>\n","\u003Cul>\n\u003Cli>Streaming removes the wait before the first token, not the work. The connection lives for the whole gap between the first token and the last.\u003C\u002Fli>\n\u003Cli>Four layers sit between the model and the browser and their idle timeouts do not agree: nginx reads 60 seconds, Cloudflare 125, Node 300, and Go has none by default.\u003C\u002Fli>\n\u003Cli>nginx times out between two successive reads, not over the whole response, so the longest silence in your stream is the number that kills it, not the total duration.\u003C\u002Fli>\n\u003Cli>SSE already gives reconnection and a Last-Event-ID header. A WebSocket adds a channel back to the server and loses both of them.\u003C\u002Fli>\n\u003Cli>When a run takes minutes, persist the events first and stream them second, because a reconnect has nothing to replay if the events only ever existed inside one response.\u003C\u002Fli>\n\u003C\u002Ful>\n",[296,301,306,325,340,355],{"id":265,"title":266,"title_id":21,"slug":267,"excerpt":268,"excerpt_id":21,"has_translation":15,"published_at":269,"reading_time":79,"hidden_from_listing":15,"tags":270,"thumbnail_image":21,"og_image":272,"series":297,"authors":298},{"id":87,"title":35,"title_id":36,"slug":37},[299],{"authors_id":300},{"id":91,"name":122,"slug":123,"role":124,"profile_picture":21},{"id":74,"title":75,"title_id":21,"slug":76,"excerpt":77,"excerpt_id":21,"has_translation":15,"published_at":78,"reading_time":79,"hidden_from_listing":15,"tags":80,"thumbnail_image":21,"og_image":85,"series":302,"authors":303},{"id":87,"title":35,"title_id":36,"slug":37},[304],{"authors_id":305},{"id":91,"name":122,"slug":123,"role":124,"profile_picture":21},{"id":307,"title":308,"title_id":309,"slug":310,"excerpt":311,"excerpt_id":312,"has_translation":12,"published_at":313,"reading_time":208,"hidden_from_listing":15,"tags":314,"thumbnail_image":21,"og_image":316,"series":317,"authors":322},133,"Python Strings: the Badge That Only Worked for One Name","String Python: Badge yang Cuma Jalan untuk Satu Nama","python-strings","The badge script printed the right name for the person who wrote it and the wrong name for everybody else. Slicing counts characters, and a name is words.","Skrip badge mencetak nama yang benar untuk orang yang menulisnya dan nama yang salah untuk semua orang lain. Slicing menghitung karakter, sedangkan nama itu kata.","2026-10-28T13:00:00.000Z",[180,315],"string","\u002Fog\u002Fpython-strings.png",{"id":318,"title":319,"title_id":320,"slug":321},30,"Collections and Strings","Koleksi dan String","python-collections",[323],{"authors_id":324},{"id":91,"name":122,"slug":123,"role":124,"profile_picture":21},{"id":326,"title":327,"title_id":328,"slug":329,"excerpt":330,"excerpt_id":331,"has_translation":12,"published_at":332,"reading_time":208,"hidden_from_listing":15,"tags":333,"thumbnail_image":21,"og_image":335,"series":336,"authors":337},132,"Python Tuples: Why All Three Points Are the Same Point","Tuple Python: Kenapa Ketiga Titik Itu Titik yang Sama","python-tuples","Three points walk into a loop and come out identical. The list was never copied three times; the name was. A tuple makes that bug impossible.","Tiga titik masuk ke satu loop dan keluar identik. List-nya nggak pernah disalin tiga kali; yang disalin cuma namanya. Tuple membuat bug ini mustahil terjadi.","2026-10-28T12:00:00.000Z",[180,334],"tuple","\u002Fog\u002Fpython-tuples.png",{"id":318,"title":319,"title_id":320,"slug":321},[338],{"authors_id":339},{"id":91,"name":122,"slug":123,"role":124,"profile_picture":21},{"id":341,"title":342,"title_id":343,"slug":344,"excerpt":345,"excerpt_id":346,"has_translation":12,"published_at":347,"reading_time":208,"hidden_from_listing":15,"tags":348,"thumbnail_image":21,"og_image":350,"series":351,"authors":352},131,"Python Sets: the Duplicate That Keeps Coming Back","Set Python: Duplikat yang Terus Balik Lagi","python-sets","A de-duplication loop that works on clean data leaves four tags where there should be two. The bug is case, and the fix is a `set`.","Loop de-duplikasi yang jalan mulus di data bersih ternyata menyisakan empat tag padahal harusnya dua. Biang keroknya huruf besar-kecil, dan obatnya `set`.","2026-10-28T11:00:00.000Z",[180,349],"set","\u002Fog\u002Fpython-sets.png",{"id":318,"title":319,"title_id":320,"slug":321},[353],{"authors_id":354},{"id":91,"name":122,"slug":123,"role":124,"profile_picture":21},{"id":356,"title":357,"title_id":358,"slug":359,"excerpt":360,"excerpt_id":361,"has_translation":12,"published_at":362,"reading_time":208,"hidden_from_listing":15,"tags":363,"thumbnail_image":21,"og_image":365,"series":366,"authors":367},130,"Python Dictionaries: the Key That Isn't There","Dictionary Python: Key yang Nggak Ada","python-dictionary","A word counter dies the first time it meets a word twice. The fix is one line; what takes longer is knowing which of a dict's three lookups to reach for.","Program penghitung kata langsung mati begitu ketemu kata yang sama dua kali. Perbaikannya cuma satu baris; yang butuh waktu lebih lama adalah tahu mau pakai yang mana dari tiga cara lookup dict.","2026-10-28T10:00:00.000Z",[180,364],"dictionary","\u002Fog\u002Fpython-dictionary.png",{"id":318,"title":319,"title_id":320,"slug":321},[368],{"authors_id":369},{"id":91,"name":122,"slug":123,"role":124,"profile_picture":21},[371,374,377,380,383,386,389],{"text":372,"depth":108,"id":373},"Streaming moves the wait, it does not remove it","streaming-moves-the-wait-it-does-not-remove-it",{"text":375,"depth":108,"id":376},"Four layers, six timeouts, no two agree","four-layers-six-timeouts-no-two-agree",{"text":378,"depth":108,"id":379},"This is not a WebSocket problem","this-is-not-a-websocket-problem",{"text":381,"depth":108,"id":382},"When the run takes four minutes, the stream is not the transport","when-the-run-takes-four-minutes-the-stream-is-not-the-transport",{"text":384,"depth":108,"id":385},"Cancelling is a decision on the server","cancelling-is-a-decision-on-the-server",{"text":387,"depth":108,"id":388},"Measure your own number","measure-your-own-number",{"text":390,"depth":108,"id":391},"What this does not cover","what-this-does-not-cover",1793755531538]