hands-on labIDE in your browser

Streaming and Prompt Caching: Time to First Token, SSE and Cheaper Prompts

Stream LLM responses and measure time to first token, parse Server-Sent Events by hand, detect a buffering gateway, reorder a prompt so the provider's prefix cache serves 97 % of it, cancel generation when the user stops, and relay a stream through your own FastAPI backend without buffering.

time
50 min
checked steps
5
level
Intermediate
setup
None
NCA-AMAD

$29.99a month with Pro

Pro opens all 5 steps of this lab and every other lab. Cancel any time.

no account needed · Pro opens all 5 steps

See Pro plans
Lab cockpit50 min · 5 stepsSession running
4 / 5 steps passingRelay without buffering · step 5 of 5
stream.py▶ Run✓ Check
# ---------- Step 5: relay the stream to your own users ----------def relay_events(client, question):    """Generator for the /ask endpoint in relay.py: stream the answer to `question` (build_messages with a guest    customer) and yield each piece of text as an SSE event, 'data: {"text": "..."}\\n\\n', the moment it arrives.    End with 'data: [DONE]\\n\\n'."""     
TerminalOutput
step 5 of 5, replayed: Relay without buffering. The code, the output and the check are the lab's own.

The job

Pennant Outdoor's support assistant answers from a 3,000-token returns and warranty policy. Customers wait too long for the first word, every request pays to read the whole policy again, and answers keep generating after people close the chat. You fix all three, measure each fix, and make sure your own backend passes the stream through.

5 steps, each checked when you finish it

A check runs your work at the end of every step. Hints and the full solution are there if you get stuck.

  1. 1

    Stream and time it

    Pennant Outdoor's support assistant answers in about 120 words.

    you writestream_chat()is_buffered()read this step below ↓

  2. 2

    Read Server-Sent Events yourself

    Under the SDK, a stream is plain text over HTTP.

    you writeparse_sse()raw_stream_text()

  3. 3

    Prompts that hit the cache

    Every request carries the same 3,000-token policy.

    you writebuild_messages()

  4. 4

    Stop when the user does

    Customers press Stop, close the tab or send a new question.

    you writestream_until()

  5. 5

    Relay without buffering

    Step 1 showed a gateway that held the whole answer back.

    you writerelay_events()

Step 1 as it appears in the lab

The lab’s own text. The hint and the solution stay inside the lab.

Step 1: Stream and time it

Pennant Outdoor's support assistant answers in about 120 words. Without streaming, the customer stares at a spinner until the last word is generated. With streaming, words appear as they are produced, so the wait that matters is time to first token (TTFT).

llmsim.py is a small model server that behaves like a hosted provider: it streams Server-Sent Events, its TTFT grows with the prompt it has to read, and it caches prompt prefixes. The Run button starts it on port 8090. Its answers are filler text; what matters here is how they arrive.

do this

1. Write stream_chat(client, messages, max_tokens): call client.chat.completions.create(...) with stream=True and stream_options={"include_usage": True}, loop over the chunks, and return {"text", "ttft", "total", "chunks", "usage"}: the joined text, seconds to the first non-empty delta.content, seconds to the end, the number of text pieces, and the final chunk.usage.model_dump().

2. Write is_buffered(result): True when the first text arrived at 80 % or more of the total time.

3. Run. It streams from the simulator and from the lab's real model gateway. Compare the two lines.

stream.py, the file you edit71 lines
"""Streaming and prompt caching for Pennant Outdoor's support assistant."""
import json
import time

import requests

POLICY = open("policy.md", encoding="utf-8").read()


# ---------- Step 1: stream and time it ----------
def stream_chat(client, messages, max_tokens=120, model="sim"):
    """Stream one completion. Returns {"text", "ttft" (seconds to the first text), "total" (seconds to the end),
    "chunks" (pieces of text received), "usage" (the final usage as a dict, or None)}."""
    # TODO (Step 1): create the completion with stream=True and stream_options={"include_usage": True}.
    # For each chunk: if chunk.choices and chunk.choices[0].delta.content, record the time of the first one (ttft)
    # and collect the text; if chunk.usage, keep chunk.usage.model_dump(). Time everything from before the call.
    raise NotImplementedError("Step 1: write stream_chat()")


def is_buffered(result):
    """True when something between you and the model held the stream back: the first text arrived at or after 80 %
    of the total time."""
    # TODO (Step 1): True when result["ttft"] is at least 80 % of result["total"]
    raise NotImplementedError("Step 1: write is_buffered()")


# ---------- Step 2: read Server-Sent Events yourself ----------
def parse_sse(lines):
    """Yield the JSON payload of every "data:" line, in order. Skip blank lines and comment lines (starting with
    ":"), accept "data:" with or without a space after it, and stop at "data: [DONE]"."""
    raise NotImplementedError("parse_sse() arrives in Step 2")


def raw_stream_text(base_url, messages, max_tokens=60):
    """POST the request with "stream": true using requests (no SDK), read the response line by line with
    iter_lines(decode_unicode=True), and return the concatenated delta text."""
    raise NotImplementedError("raw_stream_text() arrives in Step 2")


# ---------- Step 3: prompts that hit the cache ----------
INSTRUCTIONS = ("You are Pennant Outdoor's support assistant. Answer from the policy below, quote the return window "
                "or warranty that applies, and keep answers under 120 words.")


def naive_messages(customer, question, now):
    """How the prototype built its prompt: greeting and time first, then everything else."""
    return [{"role": "system", "content": f"Current time: {now}. You are talking to {customer['name']} "
                                          f"(member: {customer['member']}).\n{INSTRUCTIONS}\n\n{POLICY}"},
            {"role": "user", "content": question}]


def build_messages(customer, question, now):
    """The same information, ordered so that every request starts with the same tokens: the system message holds
    only INSTRUCTIONS and POLICY; the time and the customer's name and membership go in the user message, before
    the question."""
    return naive_messages(customer, question, now)  # Step 3 reorders this for the cache


# ---------- Step 4: stop when the user does ----------
def stream_until(client, messages, should_stop, max_tokens=400, model="sim"):
    """Stream the answer, and as soon as should_stop(text_so_far) is True, close the stream so the provider stops
    generating (and billing). Returns the text received."""
    raise NotImplementedError("stream_until() arrives in Step 4")


# ---------- Step 5: relay the stream to your own users ----------
def relay_events(client, question):
    """Generator for the /ask endpoint in relay.py: stream the answer to `question` (build_messages with a guest
    customer) and yield each piece of text as an SSE event, 'data: {"text": "..."}\\n\\n', the moment it arrives.
    End with 'data: [DONE]\\n\\n'."""
    raise NotImplementedError("relay_events() arrives in Step 5")

provided for you:llmsim.pypolicy.mdrelay.pytry_it.py

what you get

  • An editor and terminal in your browser
  • A check at the end of every step
  • Hints and the full solution
  • Nothing to install or configure

before you start

  • Python: functions, loops, generators (helpful)
  • Helpful: Tokens, Context and Cost

Exam domains covered

LLM applicationsLatencyCost

Part of the LLMOps and MLOps track

Lab 8 of 22. vLLM serving, load tests against SLOs, tracing, drift monitoring and prompt tests in CI.

Questions about this lab

What is time to first token?

The time from sending a request to receiving the first piece of the answer. With streaming it is what users perceive as the wait; the rest of the answer arrives while they read.

How does prompt caching work?

Providers cache the beginning of recent prompts. When a new request starts with exactly the same tokens (often at least 1,024 of them), that prefix is served from the cache at a lower price and with less delay. Anything that changes, such as a timestamp or a user name, must come after the stable part.

Why is my LLM stream arriving all at once?

Something on the path is buffering: a proxy, a gateway or your own backend collecting the answer before sending it. The lab detects it by comparing time to first token with total time.

Does stopping a stream stop the cost?

Only if you close the connection. If your code stops displaying the answer but keeps reading, the model keeps generating and you pay for every token.

LLM streaming and prompt caching, measured

Streaming changes what users wait for: the time to the first token instead of the whole answer. Prompt caching changes what you pay for: a repeated prefix is read once and then served cheaper and faster. Both are easy to lose, to a buffering proxy, a relay that collects before it sends, or a timestamp at the top of the prompt. In this lab you measure time to first token, parse Server-Sent Events yourself, detect buffering, reorder a prompt for the prefix cache, cancel generation early and relay a stream through FastAPI, against a local model server that behaves like a hosted provider.