LLM Observability in Production

The Payload Is the Telemetry

Tracing was added to the inference service the same way it was added to every other service. It worked beautifully for nine days. On the tenth the trace store stopped accepting writes, and the team discovered they had been ingesting more telemetry than production traffic.


The Problem at Scale

In ordinary observability a span is metadata about work. Route, status, duration, a handful of attributes. A few hundred bytes, and the body of the request is deliberately not in there, because bodies are large and rarely needed.

In LLM observability the span contains the work. The prompt and the completion are the payload, and they are the reason you instrumented anything.

  an HTTP span            ~200 to 600 bytes of attributes
  an LLM span             a 2,000 token prompt and a 500 token
                          completion, roughly 10 KB of text

Twenty to fifty times heavier, per span, and text that does not compress the way repeated metric labels do because every one is different.

The instinct from every other system you have instrumented is to keep the metadata and drop the body. Here the body is the point, and that inversion is what the rest of this course follows from.

KEY CONCEPT

In ordinary observability the request body is optional and usually omitted. In LLM observability it is the only thing that answers the question you actually have, which is why a given answer was wrong. A trace recording that a call took 3.2 seconds and produced 412 output tokens tells you nothing about that, so the usual advice to keep the envelope and discard the contents inverts completely, and every cost, retention and sampling decision in this course follows from it.


How It Works

Where the bytes come from

A single LLM call in a real application is not one span. It is a chain, and the payload appears more than once in it.

  request received                    small
  retrieval query embedded            small
  vector search, 20 chunks returned   the chunks, several KB
  prompt assembled                    the assembled prompt, several KB
  model call                          prompt AND completion
  post processing, guardrail check    often the text again

Instrument that naively and the prompt appears three or four times in one trace, because each stage records its input and its output and the output of one stage is the input of the next. A six span trace can therefore carry four copies of a document that only ever existed once.

This is the first place the volume arithmetic in the next lesson goes wrong, and it is a design decision rather than an accident: record the payload at the boundary where it changes, and reference it elsewhere.

Why you cannot simply not store it

Consider the questions you have about an LLM system:

  why was this answer wrong?              needs the prompt
  did retrieval return the right chunks?  needs the chunks
  did the model ignore the instruction?   needs prompt and completion
  why did this request cost so much?      needs the token counts, and
                                          usually the prompt to see why
  is quality degrading?                   needs samples of both

Every one needs the content. The duration and the status code answer none of them, which is why an LLM platform with metrics and no payloads has monitoring rather than observability, in exactly the sense the free Observability Fundamentals course draws that distinction.

The output is not reproducible

The second difference, and the one that raises the stakes on everything else.

In ordinary debugging a trace is a pointer. You find the slow request, you replay it, you attach a debugger. The trace is evidence that something happened and the real investigation is the reproduction.

An LLM call is not reproducible. Same prompt, same model, same parameters, different output. Temperature, sampling, routing between replicas, a provider changing a model behind a stable name.

So the trace is not a record of a reproducible event. It is the only copy. A dropped trace is not an inconvenience that costs you one data point, it is the permanent loss of the only evidence that a specific bad answer was ever produced. That is why Module 4 treats silent drops as seriously as it does, and why sampling here is harder than sampling HTTP traffic.

What this does to the rest of the design

Four consequences, each a later module:

Volume decides the architecture, because the payload dominates and the multiplication is large. Next lesson.

Sampling is genuinely hard, because the traces worth keeping are the ones with bad outputs and you do not know an output is bad at the moment you decide whether to keep it.

Retention is a privacy question, not only a cost one, because you are storing customer text rather than durations.

Cardinality lives in the payload, so the usual label discipline is necessary and insufficient.


Building and Operating It

Measure your own span size before believing any estimate, including the ones above.

# Average bytes per span, from a sample of real traces. This is the
# only number that predicts your storage, and it varies by more than
# an order of magnitude between applications.
curl -s -u "$LF_PUBLIC_KEY:$LF_SECRET_KEY" \
  "$LANGFUSE_HOST/api/public/traces?limit=50" \
  | jq '[.data[] | tojson | length] as $sizes
      | {traces: ($sizes|length),
         mean_bytes: (($sizes|add) / ($sizes|length) | floor),
         max_bytes: ($sizes|max)}'

Compare it against an ordinary service, which is the comparison that makes the case internally.

# Bytes ingested per span, LLM services against everything else.
# Expect one to two orders of magnitude between them.
sum by (service) (rate(telemetry_bytes_ingested_total[1h]))
  / sum by (service) (rate(telemetry_spans_ingested_total[1h]))

Find the payload you are storing more than once.

# Spans within one trace whose recorded input equals the previous
# span's output. Each match is a copy you are paying for twice.
curl -s -u "$LF_PUBLIC_KEY:$LF_SECRET_KEY" \
  "$LANGFUSE_HOST/api/public/traces/$TRACE_ID" \
  | jq -r '[.observations[] | {name, in: (.input|tojson|length), out: (.output|tojson|length)}]
      | . as $o | range(1; length) as $i
      | select($o[$i].in == $o[$i-1].out)
      | "DUPLICATED PAYLOAD: \($o[$i-1].name) -> \($o[$i].name) (\($o[$i].in) bytes)"'
PRO TIP

Record the payload once, at the boundary where it changes, and reference it from the spans that merely pass it along. A six span trace that stores the assembled prompt at assembly, at the model call, and again at the guardrail check is storing the same several kilobytes three times, and the two redundant copies answer no question the first one does not. This single decision routinely takes a third off ingest volume and costs nothing in debuggability, which makes it the cheapest thing in this course.


Tradeoffs and Decision Framework

Ordinary observabilityLLM observability
The span isMetadata about workThe work itself
Typical sizeHundreds of bytesKilobytes to tens of kilobytes
The body isOptional, usually droppedThe reason you instrumented
A trace isA pointer to a reproducible eventThe only copy
Dropping one costsA data pointThe only evidence it happened
Cardinality lives inLabelsLabels and the payload

Three questions before instrumenting an LLM system. How large is a span here, measured rather than assumed, since it varies enormously between applications. How many times does the payload appear in one trace, because naive instrumentation records it at every stage. And what question does each recorded field answer, as anything answering none of them is volume without value.

Default: record payloads at the boundary where they change and reference them elsewhere, measure mean span size against an ordinary service before sizing anything, and treat a dropped trace as lost evidence rather than a lost sample.


Failure Modes and Common Mistakes

Instrumenting an LLM service like an HTTP service. The defaults assume the body is optional, and here it is the point.

Recording the payload at every stage. One document becomes four copies in a single trace.

Keeping metrics and dropping payloads to control cost. You have kept the part that answers none of your questions.

Treating a dropped trace as a lost sample. The output is not reproducible, so it is the only copy.

Sizing from a published figure. Span size varies by more than an order of magnitude between applications and yours is one query away.

Assuming you can sample the interesting traces. You do not know an answer was bad at the moment you decide whether to keep it.

KNOWLEDGE CHECK

A team instruments their LLM service with the same tracing configuration they use for HTTP services, keeping span metadata and dropping request bodies to control cost. What is the consequence?

INTERVIEW QUESTION

How is observability for an LLM system different from observability for an ordinary service?