{"slug": "streaming-llm-responses-in-django-react-the-full-implementation", "title": "Streaming LLM Responses in Django + React: The Full Implementation", "summary": "A developer detailed a full-stack implementation of streaming LLM responses in Django and React, using Server-Sent Events (SSE) instead of WebSockets for one-directional communication. The approach includes a Django StreamingHttpResponse generator that yields tokens from the Anthropic API, with critical headers like X-Accel-Buffering: no to prevent Nginx buffering. The client-side uses the EventSource API in React to consume the stream, improving user experience by showing responses incrementally rather than waiting for the full output.", "body_md": "The first time we added an AI feature to a client's Django app, we did it the naive way: POST request, wait, get a response, render it. It worked. It also felt broken. Users stared at a spinner for 8 seconds, then got a wall of text. They stopped using it. Not because the AI was wrong â€” it wasn't â€” but because waiting for the full response before showing anything felt laggy in a way that ChatGPT had trained them out of tolerating.\n\nStreaming solves this. It's not just a UX nicety â€” users start reading before the response finishes, they feel like something is happening, and they're more likely to catch when the AI goes off the rails early and stop it. We've implemented streaming across a handful of production Django+React applications now. Here's the full stack: server-side, client-side, and the edge cases that'll catch you if you skip the boring infrastructure bits.\n\nWebSockets are overkill for LLM streaming. The communication is one-directional â€” the server sends chunks, the client reads them. Server-Sent Events (SSE) is the right primitive here: it's HTTP, it works through proxies, and Django supports it without any extra infrastructure.\n\nThe core view uses Django's `StreamingHttpResponse`\n\nwith a generator that yields tokens as they arrive from the LLM API:\n\n``` python\nimport json\nimport anthropic\nfrom django.http import StreamingHttpResponse\nfrom django.views.decorators.csrf import csrf_exempt\nfrom django.views.decorators.http import require_POST\n\nclient = anthropic.Anthropic()\n\ndef stream_llm_response(prompt: str, system: str = \"\"):\n    \"\"\"Generator that yields SSE-formatted chunks from Claude.\"\"\"\n    with client.messages.stream(\n        model=\"claude-opus-4-5\",\n        max_tokens=1024,\n        system=system,\n        messages=[{\"role\": \"user\", \"content\": prompt}],\n    ) as stream:\n        for text_chunk in stream.text_stream:\n            # SSE format: data: <payload>\\n\\n\n            payload = json.dumps({\"chunk\": text_chunk, \"done\": False})\n            yield f\"data: {payload}\\n\\n\"\n\n    # Signal completion\n    yield f\"data: {json.dumps({'chunk': '', 'done': True})}\\n\\n\"\n\n@require_POST\n@csrf_exempt  # Handle CSRF separately â€” see note below\ndef ai_stream_view(request):\n    body = json.loads(request.body)\n    prompt = body.get(\"prompt\", \"\").strip()\n\n    if not prompt:\n        return JsonResponse({\"error\": \"Prompt is required\"}, status=400)\n\n    response = StreamingHttpResponse(\n        stream_llm_response(prompt),\n        content_type=\"text/event-stream\",\n    )\n    response[\"Cache-Control\"] = \"no-cache\"\n    response[\"X-Accel-Buffering\"] = \"no\"  # Disable Nginx buffering\n    return response\n```\n\nThe `X-Accel-Buffering: no`\n\nheader is important. If you're running behind Nginx (you are in production), it buffers responses by default. Without that header, your users won't see any streaming â€” they'll get the whole response at once after the stream completes, which defeats the entire purpose.\n\nOn the CSRF note: SSE endpoints receive POST requests with JSON bodies, which means standard Django CSRF tokens work fine â€” you just need to include the token in your request headers. We use `@csrf_exempt`\n\nduring development and add a custom middleware-level check in production that validates the token from the `X-CSRFToken`\n\nheader.\n\n`EventSource`\n\nis the browser API for consuming SSE. It's available in every modern browser without any dependencies:\n\n``` js\nimport { useState, useCallback } from \"react\";\n\nfunction useStreamingChat() {\n  const [response, setResponse] = useState(\"\");\n  const [isStreaming, setIsStreaming] = useState(false);\n  const [error, setError] = useState<string | null>(null);\n\n  const sendPrompt = useCallback(async (prompt: string) => {\n    setResponse(\"\");\n    setError(null);\n    setIsStreaming(true);\n\n    try {\n      // POST the prompt first to get a stream token (see auth section)\n      const initRes = await fetch(\"/api/ai/stream/\", {\n        method: \"POST\",\n        headers: {\n          \"Content-Type\": \"application/json\",\n          \"X-CSRFToken\": getCsrfToken(),\n        },\n        body: JSON.stringify({ prompt }),\n      });\n\n      if (!initRes.ok) throw new Error(\"Failed to initialise stream\");\n      if (!initRes.body) throw new Error(\"No response body\");\n\n      // Use ReadableStream directly â€” more control than EventSource\n      const reader = initRes.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        const lines = buffer.split(\"\\n\\n\");\n        buffer = lines.pop() ?? \"\";\n\n        for (const line of lines) {\n          if (!line.startsWith(\"data: \")) continue;\n          const data = JSON.parse(line.slice(6));\n          if (data.done) {\n            setIsStreaming(false);\n            return;\n          }\n          setResponse((prev) => prev + data.chunk);\n        }\n      }\n    } catch (err) {\n      setError(err instanceof Error ? err.message : \"Stream failed\");\n      setIsStreaming(false);\n    }\n  }, []);\n\n  return { response, isStreaming, error, sendPrompt };\n}\n\nfunction getCsrfToken(): string {\n  return document.cookie\n    .split(\"; \")\n    .find((row) => row.startsWith(\"csrftoken=\"))\n    ?.split(\"=\")[1] ?? \"\";\n}\n```\n\nWe use `fetch`\n\nwith a `ReadableStream`\n\nreader rather than `EventSource`\n\nfor two reasons. First, `EventSource`\n\nonly supports GET requests, which means you can't POST a JSON body â€” you'd have to encode the prompt as a query parameter (ugly, limited by URL length, logged in server access logs). Second, `ReadableStream`\n\ngives you more control over the buffer and parsing logic.\n\nThe progressive render is just React state: every chunk appended to the response string, which re-renders the text as it arrives. We pair it with a simple markdown renderer so the formatting comes through correctly as the text streams in.\n\nThree things will break your streaming in production that work perfectly in development:\n\n**Gunicorn workers.** Gunicorn's default worker type (`sync`\n\n) holds the worker thread for the entire duration of the stream. If you have 4 workers and 4 concurrent streams, your app is completely blocked. Switch to `gevent`\n\nor `gthread`\n\nworkers for streaming endpoints, or run the stream endpoint on a separate Gunicorn instance with async workers.\n\n**Load balancer timeouts.** AWS ALB and most load balancers have a default idle timeout of 60 seconds. A slow LLM response generating a long answer can easily hit this. Set your load balancer's idle timeout to at least 300 seconds on paths that serve streaming responses.\n\n**Celery integration.** If your AI calls go through Celery tasks (which they often should, for queueing and retry logic), you can't stream directly from a task return value. The pattern we use: the task writes chunks to Redis as they arrive, and a thin Django view streams from Redis to the client using the same SSE pattern. Slightly more moving parts but it decouples the AI call from the HTTP connection lifecycle.\n\nStreaming makes latency *feel* lower â€” it doesn't reduce it. Your time-to-first-token is still whatever it is, which can be 1-3 seconds for large models. If that matters for your use case (real-time autocomplete, for example), streaming is the wrong solution â€” you need a faster or smaller model.\n\nStreaming also doesn't help with error recovery mid-stream. If the LLM call fails after sending 50 tokens, you've already sent those tokens to the client and committed to a partial response. We handle this with a reconnect protocol: the client stores the partial response, and if the stream dies, it can restart with context about what was already received. Most of the time we just show the partial response with a \"response interrupted\" notice and let the user re-run â€” that's good enough for the use cases we've shipped.\n\nFinally, streaming adds complexity to your response logging. You can't log the full response until the stream completes, which means your request log entry closes before the response is done. We use a post-stream hook that fires after the generator exhausts to write the full response to the database.\n\nStreaming LLM responses in Django + React is not complicated once you have the right mental model: SSE on the server, `ReadableStream`\n\non the client, and careful attention to the infrastructure layer that sits in between. The actual API integration is about 30 lines of code. The production-readiness â€” Gunicorn workers, Nginx headers, load balancer timeouts â€” takes longer to get right than the feature itself.\n\nWe've shipped this pattern across Django monoliths, Django + React SPAs, and Django as a pure API backend. The core implementation is the same each time. What changes is the infrastructure configuration, and that's the part worth spending time on.\n\n[Lycore builds production AI systems](https://www.lycore.com/ai-development-services/) for businesses â€” we implement streaming AI interfaces, full-stack Django + React applications, and production-ready LLM integrations that hold up under real user load. [Get in touch](https://www.lycore.com/contact-us/) if you want to talk through your use case.", "url": "https://wpnews.pro/news/streaming-llm-responses-in-django-react-the-full-implementation", "canonical_source": "https://dev.to/lycore/streaming-llm-responses-in-django-react-the-full-implementation-4881", "published_at": "2026-07-09 05:01:03+00:00", "updated_at": "2026-07-09 05:11:22.011751+00:00", "lang": "en", "topics": ["developer-tools", "large-language-models", "ai-products"], "entities": ["Django", "React", "Anthropic", "Claude", "Server-Sent Events", "Nginx", "EventSource", "StreamingHttpResponse"], "alternates": {"html": "https://wpnews.pro/news/streaming-llm-responses-in-django-react-the-full-implementation", "markdown": "https://wpnews.pro/news/streaming-llm-responses-in-django-react-the-full-implementation.md", "text": "https://wpnews.pro/news/streaming-llm-responses-in-django-react-the-full-implementation.txt", "jsonld": "https://wpnews.pro/news/streaming-llm-responses-in-django-react-the-full-implementation.jsonld"}}