n4nAI

Streaming LLM responses in Django with StreamingHttpResponse

Learn how to implement django streaminghttpresponse llm integration step by step, from view setup to deployment and verification, with runnable code.

n4n Team3 min read664 words

Audio narration

Coming soon — every post will get a voice note here.

Wiring a django streaminghttpresponse llm integration is the difference between a chat UI that feels alive and one that hangs for eight seconds before dumping a wall of text. Most tutorials stop at HttpResponse; here’s how to stream tokens from an LLM provider through Django without blocking your worker or lying to the client about progress.

Step 1: Understand what StreamingHttpResponse actually does

StreamingHttpResponse is a subclass of HttpResponse that accepts an iterable (usually a generator) and writes each item to the socket as it is produced. Django does not buffer the whole body. The server uses chunked transfer encoding, so the client receives bytes incrementally.

The generator runs in the request worker, which has two consequences: any middleware that tries to read response.content will break streaming, and GZipMiddleware will buffer everything until the generator exhausts. Disable compression for streaming paths.

# views.py
from django.http import StreamingHttpResponse

def ping_stream(request):
    def gen():
        for i in range(3):
            yield f"chunk {i}\n"
    return StreamingHttpResponse(gen(), content_type="text/plain")

This dummy view is the skeleton. The real work is replacing gen with a loop that pulls from an LLM API.

Step 2: Install and configure a streaming LLM client

Any OpenAI-compatible client supports stream=True. Install the official SDK and read the key from the environment, not from source.

pip install openai
# llm_client.py
import os
from openai import OpenAI

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

A streaming call returns an iterator of Chunk objects. Each chunk carries a delta with partial content:

stream = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "Explain streaming"}],
    stream=True,
)
for chunk in stream:
    delta = chunk.choices[0].delta.content
    if delta:
        print(delta)  # partial string

That delta is exactly what we want to push to the browser.

Step 3: Write the generator that yields LLM deltas

Server-Sent Events (SSE) are the most robust way to stream structured progress to a browser. Each frame is data: <json>\n\n. We wrap the LLM loop in a generator and yield SSE strings.

import json
from django.http import StreamingHttpResponse
from llm_client import client

def stream_llm_view(request):
    prompt = request.GET.get("q", "Hello")

    def generate():
        stream = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": prompt}],
            stream=True,
        )
        try:
            for chunk in stream:
                delta = chunk.choices[0].delta.content
                if delta:
                    yield f"data: {json.dumps({'text': delta})}\n\n"
        except Exception as e:
            yield f"data: {json.dumps({'error': str(e)})}\n\n"
        yield "event: done\ndata: {}\n\n"

    return StreamingHttpResponse(generate(), content_type="text/event-stream")

This is the core of a django streaminghttpresponse llm pipeline: a generator that iterates the SDK stream and emits SSE frames. If you control the client fully, plain text chunks work too, but SSE gives you a clean event: done signal.

Step 4: Handle client abort and resource cleanup

When the user closes the tab, Django calls generator.close(), which raises GeneratorExit at the yield point. If you ignore it, the generator keeps pulling tokens and burning spend. Catch it, close the upstream stream, and re-raise.

def generate():
    stream = client.chat.completions.create(...)
    try:
        for chunk in stream:
            delta = chunk.choices[0].delta.content
            if delta:
                yield f"data: {json.dumps({'text': delta})}\n\n"
    except GeneratorExit:
        stream.close()  # release the HTTP connection to the LLM
        raise
    except Exception as e:
        yield f"data: {json.dumps({'error': str(e)})}\n\n"
    yield "event: done\ndata: {}\n\n"

The OpenAI SDK’s stream object exposes close(). Always re-raise GeneratorExit so Django can tear down the response correctly.

Step 5: Choose a deployment topology that survives slow generators

A django streaminghttpresponse llm view holds a worker for the full duration of generation—often 10–60 seconds. With gunicorn’s default sync worker, one request saturates one worker. Use threaded or async workers.

Threaded WSGI is the lowest-friction fix:

gunicorn myproject.wsgi:application -k gthread --threads 8 -b 0.0.0.0:8000

Gevent works too, but mixing it with the OpenAI SDK’s urllib3 can cause monkeypatch surprises. If you move to ASGI, remember that a synchronous SDK call still blocks the event loop; you would need AsyncOpenAI and a true async generator. For most teams, gthread is enough and keeps the code above unchanged.

Step 6: Test the endpoint with curl and check headers

Start the server and hit the view with -N (no buffering) and -i (show headers):

curl -Ni "http://localhost:8000/stream/?q=hello"

Expected headers include Content-Type: text/event-stream and Transfer-Encoding: chunked. The body should appear frame by frame, not all at once:

data: {"text": "Hello"}
data: {"text": " there"}
event: done
data: {}

If you see the entire body after a delay, a middleware is buffering. Remove GZipMiddleware from the streaming route or set response["Content-Encoding"] = "identity".

Step 7: Front the LLM with a gateway for fallback

If you put a gateway such as n4n.ai in front of the model, the OpenAI-compatible endpoint provides automatic fallback when a provider is rate-limited or degraded, and it forwards provider cache-control hints. The django streaminghttpresponse llm view does not need custom retry logic; just point the client at the gateway:

client = OpenAI(
    api_key=os.environ["N4N_API_KEY"],
    base_url="https://api.n4n.ai/v1",
)

Per-token usage metering is handled upstream, so your Django logs only see the stream. The generator code from Step 3 is unchanged.

Step 8: Lock down the view

Streaming endpoints are POST-prone if you accept prompts. Use csrf_exempt only for trusted internal callers, and add login_required or a token check for external ones.

from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.decorators import login_required

@login_required
@csrf_exempt
def stream_llm_view(request):
    ...

Rate-limit by user with a simple cache counter to avoid one client exhausting your LLM quota.

Verify success in the browser

A quick EventSource test confirms progressive rendering:

const es = new EventSource('/stream/?q=why+stream');
es.onmessage = (e) => {
  const data = JSON.parse(e.data);
  if (data.text) document.body.append(data.text);
};

Open DevTools → Network, click the request, and watch the Response tab fill line by line. That is the proof your django streaminghttpresponse llm integration is live and not faking latency.

Tagsdjangostreamingllm-apihttp-response

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All django llm integration posts →