{"slug": "streaming-ai-responses-in-laravel-with-server-sent-events", "title": "Streaming AI Responses in Laravel with Server-Sent Events", "summary": "A developer upgraded a Laravel AI chatbot to stream responses using Server-Sent Events (SSE), leveraging Laravel 11's built-in `response()->eventStream()` method. The approach eliminates the 5-second wait for full completion by showing tokens as they arrive, improving perceived performance without extra infrastructure like WebSockets. The implementation uses the OpenAI PHP client's `createStreamed()` method and requires releasing the session lock to prevent blocking other requests.", "body_md": "In [the first post of this series](https://dev.to/adityakdevin/adding-an-ai-chatbot-to-your-laravel-app-with-the-openai-api-177f) we built a working AI chatbot in Laravel. It had one problem every user notices immediately: you send a message, then stare at a spinner for five seconds while the whole response generates.\n\nLLMs produce text token by token. If you wait for the full completion before showing anything, you're throwing away the single biggest UX win available: **streaming**. The same answer that takes 5 seconds to finish starts appearing in ~300ms when streamed. Nothing about the model got faster — but to the user, it feels 10x faster.\n\nIn this post we'll upgrade the chatbot to stream responses with **Server-Sent Events (SSE)** — no WebSockets, no Pusher, no extra infrastructure. Just Laravel.\n\nWebSockets are bidirectional and need a long-running server (Reverb, Soketi) or a paid service. For chat completions you only need **one direction**: server → browser, for the lifetime of one request. That's exactly what SSE is for, and since Laravel 11 it's built into the framework as `response()->eventStream()`\n\n.\n\nRule of thumb: presence, typing indicators, multiplayer → WebSockets. Streaming one AI answer → SSE.\n\nWe keep the `ChatService`\n\nfrom part 1 and add a streaming method. The `openai-php`\n\nclient exposes `createStreamed()`\n\n, which returns an iterator of deltas:\n\n``` php\n<?php\n\nnamespace App\\Services;\n\nuse Generator;\nuse OpenAI\\Laravel\\Facades\\OpenAI;\n\nclass ChatService\n{\n    // ... SYSTEM_PROMPT and reply() from part 1 ...\n\n    /**\n     * @param array<int, array{role: string, content: string}> $history\n     */\n    public function streamReply(array $history, string $userMessage): Generator\n    {\n        $stream = OpenAI::chat()->createStreamed([\n            'model' => 'gpt-4o-mini',\n            'messages' => [\n                ['role' => 'system', 'content' => self::SYSTEM_PROMPT],\n                ...$history,\n                ['role' => 'user', 'content' => $userMessage],\n            ],\n            'max_tokens' => 500,\n        ]);\n\n        foreach ($stream as $response) {\n            $delta = $response->choices[0]->delta->content;\n\n            if ($delta !== null) {\n                yield $delta;\n            }\n        }\n    }\n}\n```\n\nA `Generator`\n\nis the right return type here: the controller can forward chunks as they arrive without ever holding the full response in memory.\n\nLaravel 11.19+ ships `response()->eventStream()`\n\n, which handles the SSE formatting (`event:`\n\n/ `data:`\n\nlines) for you:\n\n``` php\n<?php\n\nnamespace App\\Http\\Controllers;\n\nuse App\\Services\\ChatService;\nuse Illuminate\\Http\\Request;\n\nclass ChatStreamController extends Controller\n{\n    public function __invoke(Request $request, ChatService $chat)\n    {\n        $validated = $request->validate([\n            'message' => ['required', 'string', 'max:2000'],\n        ]);\n\n        $history = $request->session()->get('chat_history', []);\n\n        // CRITICAL: release the session lock. A streaming response is a\n        // long-lived request — with the default file/database session\n        // driver it would block every other request from this user\n        // (including page loads!) until the stream finishes.\n        $request->session()->save();\n\n        return response()->eventStream(function () use ($request, $chat, $history, $validated) {\n            $full = '';\n\n            foreach ($chat->streamReply($history, $validated['message']) as $chunk) {\n                $full .= $chunk;\n                yield $chunk;\n            }\n\n            // persist history once the stream completes\n            $history[] = ['role' => 'user', 'content' => $validated['message']];\n            $history[] = ['role' => 'assistant', 'content' => $full];\n            $request->session()->put('chat_history', array_slice($history, -20));\n            $request->session()->save();\n        }, headers: [\n            'X-Accel-Buffering' => 'no', // tell nginx not to buffer the stream\n        ]);\n    }\n}\n```\n\nRoute it with the same throttling as part 1:\n\n``` php\nRoute::post('/chat/stream', ChatStreamController::class)\n    ->middleware(['auth', 'throttle:20,1']);\n```\n\nThe native `EventSource`\n\nAPI only supports GET requests, and we need to POST a message body — so we read the stream with `fetch`\n\ninstead:\n\n``` js\n<script>\nasync function streamChat(message, botEl) {\n    const res = await fetch('/chat/stream', {\n        method: 'POST',\n        headers: {\n            'Content-Type': 'application/json',\n            'X-CSRF-TOKEN': document.querySelector('meta[name=\"csrf-token\"]').content,\n        },\n        body: JSON.stringify({ message }),\n    });\n\n    const reader = res.body.getReader();\n    const decoder = new TextDecoder();\n    let buffer = '';\n\n    while (true) {\n        const { done, value } = await reader.read();\n        if (done) break;\n\n        buffer += decoder.decode(value, { stream: true });\n\n        // SSE messages are separated by a blank line\n        const events = buffer.split('\\n\\n');\n        buffer = events.pop(); // keep the incomplete tail\n\n        for (const evt of events) {\n            const data = evt.split('\\n')\n                .filter(l => l.startsWith('data: '))\n                .map(l => l.slice(6))\n                .join('\\n');\n\n            if (data && data !== '</stream>') {\n                botEl.textContent += data;\n            }\n        }\n    }\n}\n</script>\n```\n\n`</stream>`\n\nis Laravel's default end-of-stream marker — filter it out (or customize it with the `endStreamWith`\n\nargument).\n\nThat's it. Send a message and watch the answer type itself out.\n\nThis is the part most tutorials skip. Streaming works instantly on `php artisan serve`\n\n, then mysteriously arrives all-at-once on your server. The culprit is always **buffering somewhere between PHP and the browser**:\n\n`X-Accel-Buffering: no`\n\nheader above disables it per-response; alternatively set `proxy_buffering off;`\n\nfor the route.`output_buffering`\n\nin `php.ini`\n\n. Laravel's `eventStream`\n\nflushes after every yield, but an outer buffer can still swallow it.`curl -N`\n\nagainst production before blaming your code.Debug tip: `curl -N -X POST https://yourapp.test/chat/stream ...`\n\nshows you exactly what arrives and when, with no browser magic in between.\n\n`X-Accel-Buffering: no`\n\n`max_execution_time`\n\nand your web server's send timeout. Set both above your worst-case generation time (60s is a sane ceiling with `max_tokens: 500`\n\n).Our bot streams beautifully, but it still only knows what's in its system prompt. Next up: **RAG in Laravel — embeddings and pgvector**, where we'll give it your actual documentation to answer from. Follow me to catch it.\n\n*I'm Aditya Kumar ( adityakdevin) — Tech Lead & full-stack developer building AI-powered web products with Laravel, Vue, and LLM APIs. Find me at adityadev.in.*", "url": "https://wpnews.pro/news/streaming-ai-responses-in-laravel-with-server-sent-events", "canonical_source": "https://dev.to/adityakdevin/streaming-ai-responses-in-laravel-with-server-sent-events-3bob", "published_at": "2026-07-16 07:49:29+00:00", "updated_at": "2026-07-16 08:06:13.686106+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "developer-tools"], "entities": ["Laravel", "OpenAI", "ChatService", "ChatStreamController", "GPT-4o-mini", "SSE", "Server-Sent Events"], "alternates": {"html": "https://wpnews.pro/news/streaming-ai-responses-in-laravel-with-server-sent-events", "markdown": "https://wpnews.pro/news/streaming-ai-responses-in-laravel-with-server-sent-events.md", "text": "https://wpnews.pro/news/streaming-ai-responses-in-laravel-with-server-sent-events.txt", "jsonld": "https://wpnews.pro/news/streaming-ai-responses-in-laravel-with-server-sent-events.jsonld"}}