03 - Streaming LLM Tokens in PHP with Server-Sent Events A developer demonstrated how to stream LLM tokens in PHP using Server-Sent Events, wrapping NanoAgent's Agent::stream() callback to emit each generated token as an SSE data line. The approach requires flushing output after every token and setting the X-Accel-Buffering: no header to prevent Nginx buffering, with a [DONE] sentinel closing the client's EventSource connection. The writeup argues SSE is simpler than WebSockets for one-way token flow since it uses plain HTTP, auto-reconnects, and passes through proxies that block WebSockets. The worst part of a chat UI is the blank screen while the model thinks. Fix: stream the tokens as they're generated , not the whole reply at the end. In PHP that's Server-Sent Events SSE - one persistent response that keeps pushing data. NanoAgent's Agent::stream hands you each token through a callback. Your job is just to format it as SSE and flush. Everything below only runs when the request is asking for a stream ?stream=1&message=... . The four headers are what turn a normal PHP response into a Server-Sent Events stream the browser will keep open and read incrementally. php require once DIR . '/../NanoAgent/autoloader.php'; use NanoAgent\Agent; if isset $ GET 'stream' && empty $ GET 'message' { header 'Content-Type: text/event-stream' ; header 'Cache-Control: no-cache' ; header 'Connection: keep-alive' ; header 'X-Accel-Buffering: no' ; // critical under Nginx $userMessage = trim $ GET 'message' ; $configFile = DIR . '/../NanoAgent/config.php'; $config = file exists $configFile ? require $configFile : ; $agent = new Agent llm: 'provider' = $config 'provider' ?? 'groq', 'model' = $config 'model' ?? 'llama-3.3-70b-versatile', 'api key' = $config 'api key' ?? '' , systemPrompt: "You are a helpful and concise streaming assistant." ; stream calls your closure once for every token the model generates. Each call formats the token as an SSE data: line and forces PHP to send it immediately instead of buffering it. php $agent- stream $userMessage, function $token { echo "data: " . json encode 'token' = $token . "\n\n"; ob flush ; flush ; } ; Once the model is done, send a sentinel value the client can watch for, then close out the request. echo "data: DONE \n\n"; ob flush ; flush ; exit; } Two things are non-negotiable here: ob flush + flush after every token. X-Accel-Buffering: no . A plain EventSource - no library, no WebSocket: js const es = new EventSource /streaming.php?stream=1&message=${encodeURIComponent msg } ; es.onmessage = e = { if e.data === ' DONE ' { es.close ; return; } const { token } = JSON.parse e.data ; answerEl.textContent += token; }; For one-way token flow model → browser , SSE is simpler: it's plain HTTP, auto-reconnects, and works through proxies that choke on WebSockets. You only need a real bidirectional socket if the client has to interrupt mid-stream - which is a separate feature. stream turns "wait 8 seconds for a wall of text" into "watch it type." The entire change from a blocking chat is one callback + two flush calls + the SSE headers. Everything else in your app stays the same. Part of the NanoAgent examples series. Landing + demos https://hammrouni.github.io/nanoagentphp/ .