LLM Observability 2026: How to Monitor AI Applications in Production
A comprehensive guide to LLM observability in 2026 — covering Langfuse, Helicone, Arize AI, OpenTelemetry, token cost tracking, latency monitoring, eval pipelines, and hallucination detection with real code examples.
Shipping an LLM-powered application is the easy part. Keeping it reliable, cost-efficient, and accurate in production is where most teams struggle. LLM observability — the practice of monitoring what your models do at runtime — has become a non-negotiable engineering discipline in 2026.
This guide covers the full spectrum: from instrumenting your first LLM call to building automated eval pipelines that catch regressions before they reach users.
Why LLM Observability Is Different
Traditional APM tools were built for deterministic software. If processPayment() returns the wrong value, the cause is in the code. LLMs break that mental model entirely:
- The same prompt can produce different outputs on different calls
- Latency varies wildly based on token count, model load, and streaming behavior
- Cost is runtime-determined — a runaway agent can burn $1,000 in minutes
- Quality degrades silently — hallucinations don’t throw exceptions
- Context window management failures cause subtle, hard-to-diagnose bugs
You need observability at four levels: infrastructure, model, application, and quality.
The LLM Observability Stack in 2026
| Layer | What to Track | Tools |
|---|---|---|
| Infrastructure | Latency, errors, rate limits | Langfuse, Helicone, Arize AI |
| Token economics | Input/output tokens, cost per request | All platforms + custom billing |
| Traces | Full prompt/response chains | OpenTelemetry, LangSmith |
| Quality/Evals | Accuracy, groundedness, tone | Langfuse evals, Arize Phoenix |
Platform Deep Dive
Langfuse: Open-Source, Self-Hostable
Langfuse is the most popular open-source LLM observability platform. It traces full prompt chains, scores outputs, and integrates with every major LLM SDK.
Strengths:
- Self-hostable (Postgres + Docker) — critical for data compliance
- First-class support for multi-step agent traces (parents/children)
- Built-in eval framework for scoring outputs
- Native SDK for Python and TypeScript
Setup:
# Self-host with Docker Compose
git clone https://github.com/langfuse/langfuse
cd langfuse
docker compose up -d
Instrumenting OpenAI calls (Python):
from langfuse.openai import openai
from langfuse import Langfuse
langfuse = Langfuse()
# Drop-in replacement — all calls are automatically traced
response = openai.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Explain quantum entanglement in simple terms"}
],
# Optional: associate with a trace for multi-step visibility
langfuse_observation_id="call-xyz-123",
)
print(response.choices[0].message.content)
Tracing complex agent chains:
from langfuse.decorators import observe, langfuse_context
@observe(name="research-agent")
def research_and_synthesize(query: str) -> str:
# Step 1: Search
search_results = web_search(query)
# Step 2: Summarize each result
summaries = []
for result in search_results[:3]:
summary = summarize_document(result)
summaries.append(summary)
# Step 3: Synthesize
synthesis = synthesize_summaries(summaries, query)
# Add custom metadata to the trace
langfuse_context.update_current_observation(
metadata={
"num_sources": len(search_results),
"query_type": "research",
},
usage_details={"model": "gpt-4o", "total_tokens": 4500},
)
return synthesis
@observe(name="summarize-document")
def summarize_document(document: str) -> str:
response = openai.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "user", "content": f"Summarize this document:\n\n{document}"}
],
)
return response.choices[0].message.content
Helicone: Simple, Proxy-Based Observability
Helicone works as a proxy in front of your LLM API. Change one URL, get instant observability — no SDK required.
Setup (Python):
from openai import OpenAI
client = OpenAI(
api_key="your-openai-key",
base_url="https://oai.helicone.ai/v1",
default_headers={
"Helicone-Auth": f"Bearer {HELICONE_API_KEY}",
# Tag requests for filtering in dashboard
"Helicone-Property-Environment": "production",
"Helicone-Property-Feature": "document-qa",
"Helicone-User-Id": user_id, # Per-user cost tracking
}
)
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
)
Setup (TypeScript):
import OpenAI from 'openai';
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: 'https://oai.helicone.ai/v1',
defaultHeaders: {
'Helicone-Auth': `Bearer ${process.env.HELICONE_API_KEY}`,
'Helicone-Property-App': 'my-saas-app',
'Helicone-Cache-Enabled': 'true', // Enable semantic caching
'Helicone-Cache-Bucket-Max-Size': '10', // Cache up to 10 variations
},
});
Helicone’s semantic caching is a killer feature — it caches similar (not just identical) prompts, cutting costs dramatically for common queries.
Arize AI: Enterprise-Grade ML Observability
Arize AI is the enterprise choice, with deep support for:
- Phoenix (open-source eval/tracing tool, works standalone)
- Embedding drift detection
- Production vs. golden-set comparison
- Automated alerting on quality degradation
Phoenix quick start (local tracing):
import phoenix as px
from phoenix.otel import register
from opentelemetry.instrumentation.openai import OpenAIInstrumentor
# Start local Phoenix server
session = px.launch_app()
# Register OTEL tracer pointing to Phoenix
tracer_provider = register(project_name="my-llm-app")
# Auto-instrument OpenAI
OpenAIInstrumentor().instrument(tracer_provider=tracer_provider)
# Now all OpenAI calls are automatically traced
from openai import OpenAI
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "What is RLHF?"}],
)
print(f"Phoenix UI: {session.url}")
OpenTelemetry for LLMs
OpenTelemetry (OTel) is becoming the standard instrumentation layer for LLM observability. The OpenLLMetry spec defines semantic conventions for AI/LLM spans.
Why OTel matters: instrument once, export to any backend (Langfuse, Arize, Jaeger, Datadog, etc.)
Full Python example:
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.openai import OpenAIInstrumentor
# Configure OTel with OTLP exporter (send to Langfuse, Arize, etc.)
provider = TracerProvider()
provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(
endpoint="https://cloud.langfuse.com/api/public/otel/v1/traces",
headers={
"Authorization": f"Basic {LANGFUSE_ENCODED_KEY}",
},
)
)
)
trace.set_tracer_provider(provider)
# Instrument OpenAI
OpenAIInstrumentor().instrument()
# Custom span with LLM-specific attributes
tracer = trace.get_tracer("my-app")
with tracer.start_as_current_span("rag-pipeline") as span:
span.set_attribute("llm.system", "openai")
span.set_attribute("llm.request.model", "gpt-4o")
span.set_attribute("rag.num_documents", 5)
span.set_attribute("rag.retrieval_strategy", "hybrid")
result = run_rag_pipeline(query)
span.set_attribute("llm.response.token_count.total", result.total_tokens)
span.set_attribute("rag.answer_relevance_score", result.relevance_score)
TypeScript with OTel:
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { OpenAIInstrumentation } from '@arizeai/openinference-instrumentation-openai';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
const provider = new NodeTracerProvider();
provider.addSpanProcessor(
new BatchSpanProcessor(
new OTLPTraceExporter({
url: 'https://otlp.eu.langfuse.com/v1/traces',
headers: {
Authorization: `Basic ${process.env.LANGFUSE_BASE64_KEY}`,
},
})
)
);
provider.register();
registerInstrumentations({
instrumentations: [new OpenAIInstrumentation()],
});
Token Cost Tracking
Token costs are your LLM application’s variable COGS. Track them at three granularities:
Per-Request Cost Calculation
import anthropic
COST_PER_1K = {
"claude-3-5-sonnet-20241022": {"input": 0.003, "output": 0.015},
"claude-3-haiku-20240307": {"input": 0.00025, "output": 0.00125},
"gpt-4o": {"input": 0.005, "output": 0.015},
"gpt-4o-mini": {"input": 0.00015, "output": 0.0006},
}
def calculate_cost(model: str, input_tokens: int, output_tokens: int) -> float:
rates = COST_PER_1K.get(model, {"input": 0, "output": 0})
return (
(input_tokens / 1000) * rates["input"] +
(output_tokens / 1000) * rates["output"]
)
client = anthropic.Anthropic()
message = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
messages=[{"role": "user", "content": "Explain RAG architecture"}],
)
cost = calculate_cost(
"claude-3-5-sonnet-20241022",
message.usage.input_tokens,
message.usage.output_tokens,
)
print(f"Input tokens: {message.usage.input_tokens}")
print(f"Output tokens: {message.usage.output_tokens}")
print(f"Request cost: ${cost:.6f}")
Aggregate Cost Dashboard (TypeScript)
interface LLMUsageRecord {
timestamp: Date;
model: string;
feature: string;
userId: string;
inputTokens: number;
outputTokens: number;
costUsd: number;
latencyMs: number;
}
class LLMCostTracker {
private records: LLMUsageRecord[] = [];
track(record: LLMUsageRecord): void {
this.records.push(record);
// In production: write to your data warehouse (BigQuery, Snowflake, etc.)
}
summarize(windowMs: number = 86_400_000): CostSummary {
const cutoff = new Date(Date.now() - windowMs);
const recent = this.records.filter(r => r.timestamp > cutoff);
return {
totalRequests: recent.length,
totalCostUsd: recent.reduce((sum, r) => sum + r.costUsd, 0),
totalInputTokens: recent.reduce((sum, r) => sum + r.inputTokens, 0),
totalOutputTokens: recent.reduce((sum, r) => sum + r.outputTokens, 0),
avgLatencyMs: recent.reduce((sum, r) => sum + r.latencyMs, 0) / recent.length,
costByFeature: this.groupBy(recent, 'feature', 'costUsd'),
costByModel: this.groupBy(recent, 'model', 'costUsd'),
};
}
}
Latency Monitoring and Optimization
LLM latency has two distinct components:
- Time to first token (TTFT) — critical for streaming UX
- Total generation time — critical for batch/async workflows
Measuring TTFT in Python
import time
from openai import OpenAI
client = OpenAI()
def stream_with_ttft_measurement(prompt: str):
start_time = time.perf_counter()
first_token_time = None
stream = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
stream=True,
)
full_response = ""
for chunk in stream:
if chunk.choices[0].delta.content:
if first_token_time is None:
first_token_time = time.perf_counter()
ttft_ms = (first_token_time - start_time) * 1000
print(f"⚡ TTFT: {ttft_ms:.0f}ms")
full_response += chunk.choices[0].delta.content
total_time_ms = (time.perf_counter() - start_time) * 1000
print(f"⏱ Total time: {total_time_ms:.0f}ms")
return full_response
Latency budget targets (2026 baseline):
| Workload | Target TTFT | Target Total |
|---|---|---|
| Interactive chat | <500ms | <5s |
| Auto-complete | <200ms | <2s |
| Document analysis | <1s | <30s |
| Batch processing | N/A | <5min |
Eval Pipelines: Catching Regressions Before Users Do
Evals are automated tests for LLM quality. Unlike unit tests, they use model-based or heuristic scoring.
Types of Evals
1. Reference-free evals (no ground truth needed):
- Toxicity detection
- Hallucination likelihood (self-consistency checks)
- Response format adherence
2. Reference-based evals (requires golden dataset):
- Exact match (for structured outputs)
- Semantic similarity (embeddings comparison)
- LLM-as-judge (GPT-4o evaluates another model’s output)
LLM-as-Judge Eval (Python)
from openai import OpenAI
client = OpenAI()
EVAL_PROMPT = """You are evaluating an AI assistant's response.
Question: {question}
Response: {response}
Rate the response on:
1. Accuracy (1-5): Is the information factually correct?
2. Completeness (1-5): Does it fully answer the question?
3. Conciseness (1-5): Is it appropriately brief without losing meaning?
Respond with JSON only: {{"accuracy": N, "completeness": N, "conciseness": N, "reasoning": "..."}}"""
def evaluate_response(question: str, response: str) -> dict:
eval_response = client.chat.completions.create(
model="gpt-4o", # Use a strong model as judge
messages=[{
"role": "user",
"content": EVAL_PROMPT.format(question=question, response=response)
}],
response_format={"type": "json_object"},
temperature=0, # Deterministic for evals
)
scores = json.loads(eval_response.choices[0].message.content)
scores["overall"] = sum([
scores["accuracy"],
scores["completeness"],
scores["conciseness"]
]) / 3
return scores
# Run against a golden dataset
def run_regression_eval(test_cases: list[dict]) -> EvalReport:
results = []
for case in test_cases:
response = generate_response(case["question"])
scores = evaluate_response(case["question"], response)
results.append({**case, "scores": scores, "response": response})
avg_overall = sum(r["scores"]["overall"] for r in results) / len(results)
return EvalReport(results=results, avg_score=avg_overall, passed=avg_overall >= 3.5)
Hallucination Detection via Self-Consistency
import re
from collections import Counter
def detect_hallucination(prompt: str, n_samples: int = 5) -> float:
"""
Generate N responses to the same prompt.
Low consistency across responses = higher hallucination risk.
"""
responses = []
for _ in range(n_samples):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0.7, # Some variation to detect inconsistency
)
responses.append(response.choices[0].message.content)
# Extract key facts (simplified — use NER in production)
all_numbers = [re.findall(r'\b\d+\b', r) for r in responses]
flat_numbers = [n for numbers in all_numbers for n in numbers]
if not flat_numbers:
return 0.0 # No numbers to check
# High entropy in numbers = low consistency = high hallucination risk
counter = Counter(flat_numbers)
most_common_count = counter.most_common(1)[0][1]
consistency_score = most_common_count / len(flat_numbers)
return 1.0 - consistency_score # Higher = more hallucination risk
Platform Comparison
| Feature | Langfuse | Helicone | Arize AI / Phoenix |
|---|---|---|---|
| Open source | Yes (self-hostable) | No | Phoenix: Yes |
| Proxy-based setup | No | Yes | No |
| Multi-step traces | Excellent | Basic | Excellent |
| Evals framework | Built-in | Limited | Built-in |
| Semantic caching | No | Yes | No |
| Embedding drift | No | No | Yes |
| Pricing | Free + cloud | Free tier + usage | Free (Phoenix) + Enterprise |
| Best for | Self-hosted teams | Quick setup | Enterprise ML teams |
Production Alerting Setup
Connect your observability data to alerts:
# Example: alert when per-hour cost exceeds threshold
import boto3
import json
def check_hourly_budget(tracker: LLMCostTracker, threshold_usd: float = 50.0):
summary = tracker.summarize(windowMs=3_600_000) # 1 hour
if summary.totalCostUsd > threshold_usd:
# Send SNS alert (AWS) or Slack/PagerDuty webhook
sns = boto3.client('sns')
sns.publish(
TopicArn='arn:aws:sns:us-east-1:123456789:llm-cost-alerts',
Message=json.dumps({
"alert": "LLM cost threshold exceeded",
"hourly_cost_usd": summary.totalCostUsd,
"threshold_usd": threshold_usd,
"top_features": summary.costByFeature,
}),
Subject="⚠️ LLM Cost Alert",
)
Context Window and Memory Observability
Context window management is one of the most common production failure modes — yet most teams don’t track it at all.
What to Monitor
- Context utilization: What % of the model’s context window are you using per request?
- Truncation events: Are you silently dropping conversation history or document chunks?
- Token distribution: Input vs. output token ratio reveals prompt efficiency problems
import tiktoken
class ContextWindowMonitor:
MODEL_LIMITS = {
"gpt-4o": 128_000,
"gpt-4o-mini": 128_000,
"claude-3-5-sonnet-20241022": 200_000,
"claude-3-haiku-20240307": 200_000,
}
def __init__(self, model: str):
self.model = model
self.limit = self.MODEL_LIMITS.get(model, 8_000)
try:
self.encoder = tiktoken.encoding_for_model(model)
except KeyError:
self.encoder = tiktoken.get_encoding("cl100k_base")
def count_tokens(self, messages: list[dict]) -> int:
total = 0
for message in messages:
total += 4 # overhead per message
total += len(self.encoder.encode(message.get("content", "")))
return total
def check_utilization(self, messages: list[dict]) -> dict:
token_count = self.count_tokens(messages)
utilization = token_count / self.limit
return {
"token_count": token_count,
"limit": self.limit,
"utilization_pct": round(utilization * 100, 1),
"status": "critical" if utilization > 0.9 else "warning" if utilization > 0.75 else "ok",
}
Distributed Tracing for Multi-Agent Systems
As agentic architectures become common, observability must span multiple LLM calls, tool uses, and sub-agents.
from opentelemetry import trace
from opentelemetry.trace import SpanKind
tracer = trace.get_tracer("agent-framework")
class ObservableAgent:
def __init__(self, name: str, model: str):
self.name = name
self.model = model
def run(self, task: str, parent_context=None) -> str:
with tracer.start_as_current_span(
f"agent.{self.name}",
kind=SpanKind.CLIENT,
context=parent_context,
) as span:
span.set_attribute("agent.name", self.name)
span.set_attribute("agent.model", self.model)
span.set_attribute("agent.task", task[:200])
try:
result = self._execute(task)
span.set_attribute("agent.status", "success")
return result
except Exception as e:
span.set_attribute("agent.status", "error")
span.record_exception(e)
raise
def _execute(self, task: str) -> str:
with tracer.start_as_current_span("tool.web_search") as tool_span:
tool_span.set_attribute("tool.name", "web_search")
search_results = web_search(task)
tool_span.set_attribute("tool.result_count", len(search_results))
with tracer.start_as_current_span("llm.completion") as llm_span:
llm_span.set_attribute("llm.model", self.model)
return self._call_llm(task, search_results)
The parent-child span structure means you can see the full agent run in one waterfall view — each tool call, sub-agent delegation, and LLM completion as a nested span.
Building an Observability Dashboard
For production systems, consolidate your LLM metrics into a unified view:
Key metrics to surface on your dashboard:
| Metric | Alert Threshold | Business Impact |
|---|---|---|
| P95 latency | >3s for interactive chat | User drop-off |
| Error rate | >1% | Broken user workflows |
| Token cost/hour | >$50 | Budget overrun |
| Eval score trend | <0.1 week-over-week drop | Quality regression |
| Context utilization | >85% average | Truncation risk |
| Cache hit rate | <50% for repeated queries | Cost inefficiency |
Stack recommendation for a small team:
- Langfuse for tracing and evals (self-hosted, free)
- Grafana for dashboards (pull metrics from Langfuse or your DB)
- PagerDuty / Slack webhook for cost and error alerts
- GitHub Actions for nightly eval regression runs
Summary
LLM observability in 2026 is a multi-layer discipline:
- Langfuse for open-source, self-hostable tracing with first-class eval support
- Helicone for zero-code proxy instrumentation and semantic caching
- Arize AI/Phoenix for enterprise ML teams with embedding drift and regression detection
- OpenTelemetry as the vendor-neutral instrumentation layer
- Automated evals as your regression test suite for model quality
- Token cost tracking as your LLM COGS monitoring
The teams winning with LLMs in production aren’t just the ones with the best models — they’re the ones who know exactly what their models are doing, what they cost, and when quality drops.
Next Steps
Explore these DevPlaybook tools for AI development:
- LLM Cost Calculator — Compare token costs across models
- Prompt Template Builder — Build structured prompts with variables
- AI API Response Validator — Validate and parse LLM JSON outputs
- OpenAI Token Counter — Count tokens before you send requests
DevPlaybook Pro members get access to production-ready LLM monitoring templates, eval datasets, and architecture guides for RAG applications. Upgrade to Pro →
50 CLI Commands That Save Hours Every Week
The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.
- Git mastery (10 cmds)
- File & search wizardry
- Docker & DevOps tricks
- Process & network tools
- Productivity boosters
- macOS & Linux install guide
No spam. 1 email/week max. Unsubscribe anytime.
Found this guide useful? Check out our free developer tools.
Affiliate disclosure: Some links below are affiliate links — we may earn a small commission at no extra cost to you. Learn more.
Recommended Tools & Resources
DigitalOcean
$200 credit for new users. Simple, affordable cloud hosting for developers.
GitHub Student Pack
Free access to 100+ developer tools. Perfect for students and new devs.
Vercel
Deploy frontend apps instantly. Free tier is generous for side projects.
DevPlaybook Products
Boilerplates, scripts & AI toolkits to 10x your dev workflow.