n4nAI

Streaming LLM responses from Cloud Run with HTTP/2

Learn how to deploy a Cloud Run service that streams LLM responses over HTTP/2, with runnable code and verification steps for engineers.

n4n Team3 min read726 words

Audio narration

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

Most LLM SDKs default to HTTP/1.1 and buffer the full completion before returning. If you serve generative features from Google Cloud Run, you can deliver token deltas to clients the moment they arrive by combining Server-Sent Events with cloud run http/2 llm streaming. This guide walks through a production-shaped deployment that proxies an OpenAI-compatible chat endpoint and streams chunks without buffering.

Step 1: Scaffold a FastAPI streaming endpoint

FastAPI’s StreamingResponse hands bytes to the ASGI server as they are yielded. Wrap the LLM call in an async generator and emit SSE frames.

# main.py
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
import httpx
import os

app = FastAPI()

UPSTREAM = os.environ["LLM_BASE_URL"]  # e.g. https://api.openai.com/v1
API_KEY = os.environ["LLM_API_KEY"]

async def token_stream(prompt: str):
    headers = {"Authorization": f"Bearer {API_KEY}"}
    payload = {
        "model": "gpt-4o-mini",
        "messages": [{"role": "user", "content": prompt}],
        "stream": True,
    }
    # httpx with http2=True reuses a single connection for multiplexed streams
    async with httpx.AsyncClient(http2=True, timeout=60.0) as client:
        async with client.stream("POST", f"{UPSTREAM}/chat/completions",
                                 json=payload, headers=headers) as resp:
            async for line in resp.aiter_lines():
                if line.startswith("data:"):
                    yield f"{line}\n\n"

@app.post("/stream")
async def stream(request: Request):
    body = await request.json()
    return StreamingResponse(token_stream(body["prompt"]),
                             media_type="text/event-stream")

The generator yields raw SSE lines from the upstream. No intermediate list accumulates tokens. SSE is the right wire format here: browsers and curl parse it natively, and it works over both HTTP/1.1 chunked transfer and HTTP/2 frames.

Step 2: Configure the LLM client for streaming and HTTP/2

The httpx.AsyncClient(http2=True) above negotiates HTTP/2 with the model provider when the provider supports it. If you front multiple vendors through a gateway, point LLM_BASE_URL at a single OpenAI-compatible endpoint. n4n.ai exposes one such endpoint that addresses 240+ models and applies automatic fallback when a provider is rate-limited, while still honoring stream: true and forwarding cache-control hints.

When using the official OpenAI Python SDK instead of raw httpx, pass stream=True and iterate completion.choices[0].delta.content. The SDK uses httpx under the hood; force HTTP/2 by constructing the client with a custom http_client.

from openai import AsyncOpenAI
import httpx

client = AsyncOpenAI(
    base_url="https://your-gateway.example/v1",
    api_key="sk-...",
    http_client=httpx.AsyncClient(http2=True),
)

async def openai_stream(prompt: str):
    async for chunk in await client.chat.completions.create(
        model="anthropic/claude-3.5-sonnet",
        messages=[{"role": "user", "content": prompt}],
        stream=True,
    ):
        if chunk.choices[0].delta.content:
            yield f"data: {chunk.choices[0].delta.content}\n\n"

Keep the upstream connection HTTP/2 to avoid head-of-line blocking when many clients stream concurrently. HTTP/2 multiplexing lets one TCP connection carry dozens of simultaneous token streams to the provider.

Step 3: Containerize for Cloud Run

Cloud Run injects PORT and expects the server to bind 0.0.0.0 on that port. Use Gunicorn with the Uvicorn worker class; the default sync worker will block and buffer.

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY main.py .
ENV PORT=8080
CMD exec gunicorn main:app \
    --worker-class uvicorn.workers.UvicornWorker \
    --bind 0.0.0.0:$PORT \
    --timeout 0 \
    --graceful-timeout 30

--timeout 0 disables Gunicorn’s per-request timeout so long generations don’t get killed; rely on Cloud Run’s request timeout instead. requirements.txt needs fastapi, gunicorn, uvicorn, httpx. The Uvicorn worker speaks ASGI and flushes each yield to the transport immediately.

Step 4: Deploy with HTTP/2 enabled

Cloud Run terminates HTTP/2 at the edge by default and forwards HTTP/1.1 to the container. For true end-to-end cloud run http/2 llm streaming, enable the HTTP/2 flag so the container also receives H2. This matters if you later add gRPC or want to inspect :method pseudo-headers.

gcloud run deploy llm-stream \
  --source . \
  --region us-central1 \
  --use-http2 \
  --concurrency 20 \
  --timeout 300 \
  --memory 512Mi \
  --min-instances 0 \
  --max-instances 10 \
  --set-env-vars LLM_BASE_URL=https://your-gateway.example/v1

--concurrency 20 caps simultaneous streams per instance; each open stream holds a connection, so size this against memory. --timeout 300 lets a slow 30K-token completion finish. If you skip --use-http2, streaming still works because chunked HTTP/1.1 responses are passed through, but the keyword scenario assumes H2 from edge to container.

Step 5: Verify the stream

Deploy, then hit the endpoint with curl -N to disable curl’s own buffering:

curl -N -X POST https://llm-stream-xyz.a.run.app/stream \
  -H "Content-Type: application/json" \
  -d '{"prompt":"Explain HTTP/2 multiplexing in one sentence."}'

You should see lines prefixed with data: arriving incrementally, not all at once after a pause. To confirm the transport, run a verbose POST:

curl -v -N -X POST https://llm-stream-xyz.a.run.app/stream \
  -H "Content-Type: application/json" \
  -d '{"prompt":"hi"}' 2>&1 | grep -E "HTTP/2|> :"

The request line should show > HTTP/2. In a browser, consume the stream with fetch and getReader():

const res = await fetch("/stream", {method:"POST", body: JSON.stringify({prompt:"hi"})});
const reader = res.body.getReader();
while (true) {
  const {value, done} = await reader.read();
  if (done) break;
  console.log(new TextDecoder().decode(value));
}

If tokens print one by one in the console, cloud run http/2 llm streaming is working.

Step 6: Handle client disconnects gracefully

When a browser tab closes, the HTTP/2 stream resets. Your async generator must catch the disconnect and close the upstream connection to avoid leaking sockets.

async def token_stream(prompt: str, request: Request):
    try:
        async with httpx.AsyncClient(http2=True, timeout=60.0) as client:
            async with client.stream("POST", f"{UPSTREAM}/chat/completions",
                                     json=payload, headers=headers) as resp:
                async for line in resp.aiter_lines():
                    if await request.is_disconnected():
                        break
                    yield f"{line}\n\n"
    except asyncio.CancelledError:
        # client gone, upstream will be closed by context manager
        raise

FastAPI’s request.is_disconnected() polls the receive channel. Breaking the loop lets the async with blocks exit and release the H2 connection. Also log token counts for cost tracking; if your gateway provides per-token usage metering, read the final data: [DONE] frame and emit a metric.

Pitfalls and tuning

Timeouts: Cloud Run kills the request at the service timeout. Send a comment keep-alive (yield ": ping\n\n") every 15s if your model goes silent mid-generation.

Buffering proxies: If you put a CDN in front, ensure it does not buffer. Cloud Run’s own edge does not buffer SSE.

Concurrency vs. cost: Streaming connections are long-lived. A single instance with --concurrency 20 can serve 20 simultaneous streams; beyond that requests queue. Monitor container/instance_count and adjust.

Upstream rate limits: A 429 mid-stream breaks the generator. Using a gateway with automatic fallback avoids client-visible errors; wrap client.stream in try/except and yield an error event.

HTTP/2 to the browser: Browsers open at most 6 connections per host over HTTP/1.1; with HTTP/2 multiplexing, many streams share one connection. That is the concrete win for cloud run http/2 llm streaming when a page opens several model calls at once.

Ship the container, deploy with the flags above, and verify with curl -N. The pattern scales to any OpenAI-compatible backend and keeps your users watching tokens appear instead of staring at a spinner.

Tagscloud-runstreaminghttp2llm-api

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 google cloud functions & cloud run llm integration posts →