LLM Load Testing and SLOs: TTFT, TPOT, Open-Loop Traffic, Goodput and Capacity
Hands-on lab · IDE in your browser

LLM Load Testing and SLOs: TTFT, TPOT, Open-Loop Traffic, Goodput and Capacity

Load-test a streaming LLM server the way production traffic will hit it.

Time
50 min
Checked steps
5
Level
Intermediate
Setup
None
Read step 1

Hands-on labs require Pro · $29.99/mo · cancel anytime

Lab cockpit50 min · 5 stepsSession running
4 / 5 steps passingCapacity and replicas · step 5 of 5
loadtest.py▶ Run✓ Check
# ---------- Step 5: capacity and replicas ---------- def find_capacity(measure, lo, hi, tol=0.5, slo=SLO):    """The highest request rate that meets every SLO target, to within tol. measure(rate) runs a load test and    returns its summarize(). Assumes hi fails; returns None if lo already fails."""          def replicas_for(peak_rate, capacity, headroom=0.75):    """Replicas needed to serve peak_rate when each may run at only headroom x its measured capacity.""" 
TerminalOutput

The job

Kestrel Legal is moving its contract assistant onto its own model server. Before launch someone has to say how much traffic one replica can take and how many replicas to run. You load-test a staging replica until the numbers hold up.

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

    One request, three numbers

    Kestrel Legal's contract assistant is moving from a hosted API to its own model server, and the launch plan needs two answers: how much traffic one replica can take, and how many replicas the launch needs.

    You writestream_one()
  2. 2

    An open-loop load generator

    The load test many teams write first is a closed loop: N simulated users, each sending a request, waiting for the answer, then sending the next.

    You writerun_load()
  3. 3

    Traffic that looks like production

    A load test proves only what it sends.

    You writepoisson_schedule()
  4. 4

    The SLO report

    A service level objective turns "fast enough" into numbers that a test can fail.

    You writesummarize()
  5. 5

    Capacity and replicas

    A replica's capacity is the highest arrival rate at which every SLO target still holds.

    You writefind_capacity()

Step 1 as it appears in the lab

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

Step 1: One request, three numbers

Kestrel Legal's contract assistant is moving from a hosted API to its own model server, and the launch plan needs two answers: how much traffic one replica can take, and how many replicas the launch needs. Nobody load-tests a shared production endpoint, so servesim.py runs a staging replica here. It batches requests like vLLM does.

Users feel a streamed answer in three numbers. TTFT (time to first token) is the wait before anything appears. TPOT (time per output token) is the gap between the tokens that follow, which decides whether the text keeps up with reading. e2e is the time to the last token.

Do this

1. Write stream_one() in loadtest.py. A request that fails is still a result: keep its status (0 when the connection fails) and return None for its times.

2. Run it. It sends one request, then one with a 4,000-token prompt, then 16 at once. Predict first: which of the three numbers grows when 16 users share the server?

loadtest.py, the file you edit86 lines
"""Load tests for Kestrel Legal's model server. You write the functions marked TODO, one step at a time."""
import asyncio
import json
import math
import random
import time

import httpx
import numpy as np

URL = "http://127.0.0.1:8300/v1/chat/completions"
SLO = {"ttft_p95": 1.0, "tpot_p95": 0.050, "error_rate": 0.01}   # seconds, seconds, fraction


def prompt_of(tokens):
    """A prompt the server counts as `tokens` tokens (it counts one per word)."""
    return "word " * tokens


def load_traffic(path="traffic.jsonl"):
    """A day of production requests: [{"prompt_tokens": ..., "output_tokens": ...}, ...]."""
    with open(path) as f:
        return [json.loads(line) for line in f if line.strip()]


# ---------- Step 1: one request, three numbers ----------

async def stream_one(client, url, prompt_tokens, output_tokens):
    """Send one streaming chat request and time it. Returns {"status", "ttft", "tpot", "e2e", "output_tokens"}:
    ttft = seconds from sending to the first content token, e2e = seconds to the end of the stream,
    tpot = (e2e - ttft) / (output_tokens - 1), 0.0 for a single token. A request that fails has its HTTP status
    (0 if the connection failed) and None for the three times."""
    # TODO (Step 1): build the body (model, messages, max_tokens, stream=True); start = time.perf_counter();
    # async with client.stream("POST", url, json=body) as r: return a failure record if r.status_code != 200;
    # read r.aiter_lines(), json.loads each "data:" line up to [DONE], count chunks whose delta has content
    # and time the first. Catch httpx.HTTPError as status 0.
    raise NotImplementedError("Step 1: write stream_one()")


# ---------- Step 2: an open-loop load generator ----------

async def run_load(url, schedule):
    """Send every request of schedule, a list of (at_seconds, prompt_tokens, output_tokens), at its time,
    whether or not earlier requests have finished. Returns stream_one()'s records in schedule order, each with
    "at" added."""
    raise NotImplementedError("run_load() arrives in Step 2")


# ---------- Step 3: traffic that looks like production ----------

def poisson_schedule(rate, duration, traffic, seed=0):
    """Arrivals of a Poisson process with `rate` requests per second for `duration` seconds, each with the lengths
    of a random row of traffic: a list of (at, prompt_tokens, output_tokens), times ascending and below duration."""
    raise NotImplementedError("poisson_schedule() arrives in Step 3")


# ---------- Step 4: the SLO report ----------

def summarize(records, duration):
    """{"requests", "error_rate", "ttft_p50", "ttft_p95", "tpot_p50", "tpot_p95", "e2e_p95", "throughput_rps",
    "tokens_per_s"}. Latency percentiles are over successful requests (np.percentile), inf if none succeeded;
    throughput counts successful requests and their output tokens per second of duration."""
    raise NotImplementedError("summarize() arrives in Step 4")


def meets_slo(summary, slo=SLO):
    """{target: True/False} for every target in slo: the summary's value must be at or below the target."""
    raise NotImplementedError("meets_slo() arrives in Step 4")


def goodput(records, duration, slo=SLO):
    """Requests per second of duration that succeeded with their own ttft and tpot within the SLO's p95 targets."""
    raise NotImplementedError("goodput() arrives in Step 4")


# ---------- Step 5: capacity and replicas ----------

def find_capacity(measure, lo, hi, tol=0.5, slo=SLO):
    """The highest request rate that meets every SLO target, to within tol. measure(rate) runs a load test and
    returns its summarize(). Assumes hi fails; returns None if lo already fails."""
    raise NotImplementedError("find_capacity() arrives in Step 5")


def replicas_for(peak_rate, capacity, headroom=0.75):
    """Replicas needed to serve peak_rate when each may run at only headroom x its measured capacity."""
    raise NotImplementedError("replicas_for() arrives in Step 5")
Provided for you:servesim.pytraffic.jsonltry_it.py

Frequently asked questions

What are TTFT and TPOT?

Time to first token is how long a user waits before a streamed answer starts; it includes queueing and prompt processing. Time per output token is the gap between the following tokens and decides whether the text keeps up with reading.

What is wrong with a closed-loop load test?

Each simulated user waits for an answer before sending the next request, so when the server slows down the test sends less traffic. It never builds the queue that real, independently arriving users would, and reports latency that is too good.

What is goodput?

The rate of requests that completed within the latency targets, each judged on its own TTFT and TPOT. Throughput can rise while users wait longer; goodput only rises when more requests are good enough.

Load testing an LLM endpoint

LLM servers batch many requests together, so their latency depends on load in ways a single request never shows. A useful load test measures what users feel, sends traffic the way users do, and ends in a number someone can plan with. In this lab you time streamed responses (TTFT, TPOT and end-to-end), write an open-loop async load generator, replay a production mix of prompt and output lengths with Poisson arrivals, turn the results into an SLO report with goodput, and bisect for capacity to size a launch.