{"slug": "03-streaming-llm-tokens-in-php-with-server-sent-events", "title": "03 - Streaming LLM Tokens in PHP with Server-Sent Events", "summary": "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.", "body_md": "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.\n\nNanoAgent's `Agent::stream()` hands you each token through a callback. Your job is just to format it as SSE and flush.\n\nEverything 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.\n\n``` php\nrequire_once __DIR__ . '/../NanoAgent/autoloader.php';\nuse NanoAgent\\Agent;\n\nif (isset($_GET['stream']) && !empty($_GET['message'])) {\n    header('Content-Type: text/event-stream');\n    header('Cache-Control: no-cache');\n    header('Connection: keep-alive');\n    header('X-Accel-Buffering: no');   // critical under Nginx\n\n    $userMessage = trim($_GET['message']);\n\n    $configFile = __DIR__ . '/../NanoAgent/config.php';\n    $config = file_exists($configFile) ? require $configFile : [];\n\n    $agent = new Agent(\n        llm: [\n            'provider' => $config['provider'] ?? 'groq',\n            'model'    => $config['model']    ?? 'llama-3.3-70b-versatile',\n            'api_key'  => $config['api_key']  ?? ''\n        ],\n        systemPrompt: \"You are a helpful and concise streaming assistant.\"\n    );\n```\n\n`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.\n\n``` php\n    $agent->stream($userMessage, function ($token) {\n        echo \"data: \" . json_encode(['token' => $token]) . \"\\n\\n\";\n        ob_flush();\n        flush();\n    });\n```\n\nOnce the model is done, send a sentinel value the client can watch for, then close out the request.\n\n```\n    echo \"data: [DONE]\\n\\n\";\n    ob_flush();\n    flush();\n    exit;\n}\n```\n\nTwo things are non-negotiable here:\n\n`ob_flush()` + `flush()` after every token.`X-Accel-Buffering: no`.\nA plain `EventSource` - no library, no WebSocket:\n\n``` js\nconst es = new EventSource(`/streaming.php?stream=1&message=${encodeURIComponent(msg)}`);\n\nes.onmessage = (e) => {\n  if (e.data === '[DONE]') { es.close(); return; }\n  const { token } = JSON.parse(e.data);\n  answerEl.textContent += token;\n};\n```\n\nFor 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.\n\n`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.\n\n*Part of the NanoAgent examples series. [Landing + demos](https://hammrouni.github.io/nanoagentphp/).*", "url": "https://wpnews.pro/news/03-streaming-llm-tokens-in-php-with-server-sent-events", "canonical_source": "https://dev.to/hammrouni/03-streaming-llm-tokens-in-php-with-server-sent-events-3k3a", "published_at": "2026-09-27 18:07:55+00:00", "updated_at": "2026-09-27 18:31:05.866071+00:00", "lang": "en", "topics": ["large-language-models", "ai-agents", "developer-tools", "ai-tools"], "entities": ["NanoAgent", "PHP", "Groq", "Llama 3.3 70B", "Nginx", "Server-Sent Events"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/03-streaming-llm-tokens-in-php-with-server-sent-events", "markdown": "https://wpnews.pro/news/03-streaming-llm-tokens-in-php-with-server-sent-events.md", "text": "https://wpnews.pro/news/03-streaming-llm-tokens-in-php-with-server-sent-events.txt", "jsonld": "https://wpnews.pro/news/03-streaming-llm-tokens-in-php-with-server-sent-events.jsonld"}}