{"id":102,"date":"2026-09-16T23:58:14","date_gmt":"2026-09-16T15:58:14","guid":{"rendered":"https:\/\/wp.qoraapi.com\/llm-observability\/"},"modified":"2026-09-20T03:53:19","modified_gmt":"2026-09-19T19:53:19","slug":"llm-observability","status":"publish","type":"post","link":"https:\/\/qoraapi.com\/blog\/llm-observability\/","title":{"rendered":"LLM Observability: Monitoring AI API Usage, Latency and Cost"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><strong>LLM observability<\/strong> is the practice of capturing a structured record of every AI API request \u2014 model, prompt and completion tokens, latency, cost, and error type \u2014 so you can see what your models actually cost, how fast they respond, and where they fail. It combines three things: request tracing, time-series metrics, and token-level cost accounting.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Without it, every question about your AI spend is a guess. With it, questions like &#8220;which feature is burning the budget&#8221; and &#8220;did last night&#8217;s prompt change make responses slower&#8221; become queries you run in seconds.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Why AI APIs are harder to observe than normal endpoints<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A conventional web request is easy to monitor: status code, duration, payload size. An LLM call breaks all three assumptions. It returns HTTP 200 even when the answer is wrong. Its duration depends on how many tokens it chose to generate, not on how much work your server did. And its cost is variable \u2014 the same endpoint can bill you a fraction of a cent or several cents depending on what the model decided to write.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That last point is the one that catches teams out. With a normal API, a traffic spike and a cost spike are the same event. With an LLM API, cost is driven by <em>token volume<\/em>, which is driven by prompt design, retrieved context size, and how verbose the model decides to be. A feature that is barely used can dominate the bill. Observability is how you find out before the invoice does.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The five signals worth capturing<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">You can log everything, but only a handful of fields change decisions. Capture these on every call:<\/p>\n\n\n\n<figure class=\"wp-block-table is-style-stripes\"><table class=\"has-fixed-layout\"><thead><tr><th>Signal<\/th><th>What it tells you<\/th><th>Typical use<\/th><\/tr><\/thead><tbody><tr><td>Model + provider<\/td><td>Which endpoint actually answered<\/td><td>Routing audits, A\/B comparisons<\/td><\/tr><tr><td>Input \/ output tokens<\/td><td>Volume, and therefore cost<\/td><td>Budgets, per-feature attribution<\/td><\/tr><tr><td>Time to first token (TTFT)<\/td><td>Perceived responsiveness<\/td><td>UX tuning, streaming health<\/td><\/tr><tr><td>Total latency<\/td><td>Wall-clock duration<\/td><td>Timeouts, retry policy tuning<\/td><\/tr><tr><td>Status \/ error class<\/td><td>429, 5xx, timeout, content filter<\/td><td>Reliability and capacity planning<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Two more fields are cheap to add and disproportionately useful: a <strong>request ID<\/strong> so you can trace one user action across multiple model calls, and a <strong>feature or route tag<\/strong> so you can attribute spend to the part of the product that caused it. Without the tag, all your cost data collapses into one undifferentiated number.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Instrumenting a call<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The good news is that OpenAI-compatible APIs return token usage in the response, so instrumentation is a wrapper rather than a rewrite. Here is the minimal version that captures everything in the table above:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import time, logging, json\n\nlog = logging.getLogger(\"llm\")\n\ndef observed_call(client, *, feature, model, messages, **kw):\n    t0 = time.perf_counter()\n    ttft = None\n    try:\n        resp = client.chat.completions.create(model=model, messages=messages, **kw)\n        ttft = time.perf_counter() - t0          # non-streaming: TTFT ~= total\n\n        u = resp.usage\n        log.info(json.dumps({\n            \"event\":      \"llm_call\",\n            \"feature\":    feature,               # cost attribution tag\n            \"model\":      resp.model,            # what actually answered\n            \"in_tokens\":  u.prompt_tokens,\n            \"out_tokens\": u.completion_tokens,\n            \"ttft_ms\":    round(ttft * 1000, 1),\n            \"total_ms\":   round((time.perf_counter() - t0) * 1000, 1),\n            \"status\":     \"ok\",\n        }))\n        return resp\n\n    except Exception as e:\n        log.warning(json.dumps({\n            \"event\":    \"llm_call\",\n            \"feature\":  feature,\n            \"model\":    model,\n            \"total_ms\": round((time.perf_counter() - t0) * 1000, 1),\n            \"status\":   type(e).__name__,        # RateLimitError, APITimeoutError, ...\n        }))\n        raise\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Two details make this worth more than a naive log line. First, the model is read from <em>the response<\/em>, not from your request \u2014 a relay or gateway may route to a different model than you asked for, and you want your cost data to reflect reality. Second, failures are logged with the same shape as successes, so error rates and latency percentiles come from one dataset instead of two.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Latency: measure TTFT, not just the average<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Average latency is the most misleading metric in AI products. A chatbot that streams its first token in 400 ms and finishes in 6 seconds has an average of over 3 seconds, yet users perceive it as fast. A batch summarizer that takes 9 seconds and shows nothing until it is done has the same average and feels broken.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Split the metric in two. <strong>TTFT<\/strong> governs perceived speed and should be tracked at p50, p95, and p99. <strong>Total duration<\/strong> governs throughput, timeouts, and retry behavior. Track them separately and you will stop &#8220;optimizing&#8221; latency in ways that make the product feel worse.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Streaming also changes what you can measure. When the response arrives as Server-Sent Events, the final usage block may only be present if you explicitly request it, so instrument the stream itself rather than waiting for a single response object. Our <a href=\"https:\/\/qoraapi.com\/blog\/ai-api-streaming-sse\/\">streaming and SSE guide<\/a> covers the wire format and where the usage counts appear.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Token tracking and cost dashboards<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Cost is the metric that gets executive attention, so build it properly. Never hard-code prices into your dashboard; store tokens and multiply by a price table you update in one place. Prices change, and a dashboard that silently reports stale numbers is worse than no dashboard.<\/p>\n\n\n\n<figure class=\"wp-block-table is-style-stripes\"><table class=\"has-fixed-layout\"><thead><tr><th>Dashboard panel<\/th><th>Breakdown<\/th><th>Question it answers<\/th><\/tr><\/thead><tbody><tr><td>Spend over time<\/td><td>Daily, by feature<\/td><td>Are we trending toward a runaway?<\/td><\/tr><tr><td>Cost per request<\/td><td>p50 \/ p95 by model<\/td><td>Is a prompt change inflating prompts?<\/td><\/tr><tr><td>Token mix<\/td><td>Input vs output tokens<\/td><td>Are we paying for context we do not need?<\/td><\/tr><tr><td>Latency<\/td><td>TTFT and total, p95<\/td><td>Did the last deploy make us slower?<\/td><\/tr><tr><td>Error rate<\/td><td>By class (429, 5xx, timeout)<\/td><td>Are we capacity-limited or buggy?<\/td><\/tr><tr><td>Top consumers<\/td><td>By user or API key<\/td><td>Who is generating the volume?<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">If you build only two of these panels, build <em>spend over time by feature<\/em> and <em>token mix<\/em>. The first finds the runaway. The second explains it: input tokens climbing while output stays flat almost always means retrieved context is growing \u2014 the classic RAG leak. Our <a href=\"https:\/\/qoraapi.com\/blog\/reduce-ai-api-costs\/\">AI API cost reduction guide<\/a> covers the fixes, from prompt compression to caching and routing.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Error telemetry: 429s are a capacity signal<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Error class matters more than error count. A rising 429 rate is not a bug in your code \u2014 it is your provider telling you that you are exceeding a rate or quota limit, and it usually precedes a user-visible outage. A rising timeout rate often means a specific model has degraded or your own retry logic is amplifying load. A rising content-filter rate is a product signal about what your users are sending.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Tag each class separately, and alert on the rate of change rather than the absolute number. The handling patterns \u2014 exponential backoff with jitter, fallback chains, and request queues \u2014 are covered in our <a href=\"https:\/\/qoraapi.com\/blog\/ai-api-rate-limits-429-errors\/\">guide to AI API rate limits and 429 errors<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Tracing multi-step and RAG requests<\/h2>\n\n\n<p class=\"wp-block-paragraph\">Single calls are easy. The interesting failures happen in chains: retrieve documents, re-rank, summarize, then answer. When that pipeline gets slow or expensive, an aggregate metric cannot tell you which stage is responsible.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The fix is a trace: one trace ID per user action, one span per model call, with parent-child relationships. A trace view turns &#8220;the assistant got slow&#8221; into &#8220;the re-ranking call grew from 4 retrieved chunks to 40.&#8221; You do not need a heavyweight platform to start \u2014 a correlation ID in your logs, plus a table of spans, gets you 80% of the value on day one.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Prompts, privacy, and retention<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">There is a version of this that goes wrong: a team logs full prompts and completions to make debugging easier, and three months later discovers customer data sitting in a log index with no retention policy. Observability and data protection have to be designed together.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A workable split is to always store <em>metadata<\/em> and to sample <em>content<\/em>. Metadata \u2014 tokens, latency, model, status, feature tag \u2014 carries almost all the operational value and contains no user data. Prompt and completion text is what you need for a narrow class of debugging, so store it for a short window, on a sample, or behind an explicit flag that is off by default. Hash or drop anything that looks like an identifier before it leaves your process.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Retention follows from the same logic. Cost and latency aggregates should live for a year or more, because they are how you spot slow drift. Raw payloads should expire in days. Teams that separate the two find they can answer nearly every operational question without ever keeping a customer&#8217;s conversation on disk.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">A minimal event schema you can standardise on<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Whichever backend you use, agree on one event shape. It makes dashboards reusable, makes switching providers a config change, and stops the slow drift into five incompatible log formats:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"event\":       \"llm_call\",\n  \"trace_id\":    \"a41f9c2e\",          \/\/ one per user action\n  \"span_id\":     \"span-3\",            \/\/ one per model call in the chain\n  \"feature\":     \"chat.support\",      \/\/ cost attribution tag\n  \"model\":       \"gpt-4o-mini\",       \/\/ from the response, not the request\n  \"provider\":    \"relay\",             \/\/ who actually served it\n  \"in_tokens\":   1840,\n  \"out_tokens\":  212,\n  \"ttft_ms\":     412,\n  \"total_ms\":    2317,\n  \"status\":      \"ok\",                \/\/ or RateLimitError, APITimeoutError\n  \"streamed\":    true,\n  \"ts\":          \"2026-09-16T15:55:02Z\"\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Six of those fields drive every chart in this article. The other six exist so that when something goes wrong at 3 a.m. you can go from an alert to the exact request without guesswork. Build the schema once, keep it boring, and resist the urge to add fields that only one dashboard reads.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Alerts that actually catch runaways<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Daily spend above a rolling baseline.<\/strong> Compare today to the trailing seven-day median, not to a fixed number \u2014 traffic grows.<\/li>\n<li><strong>Cost per request drifting up.<\/strong> A prompt or context change that doubles average input tokens is invisible until you chart it per request.<\/li>\n<li><strong>p95 TTFT regression after a deploy.<\/strong> Attach the deploy marker to your charts and the correlation is immediate.<\/li>\n<li><strong>429 rate above a threshold.<\/strong> Treat it as a capacity warning, not an error to be retried silently.<\/li>\n<li><strong>Any single API key exceeding a share of total volume.<\/strong> Usually a runaway script, a leaked key, or an accidental loop.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">All five are cheap to compute from the log line above. None of them require a dedicated observability vendor, which matters because the fastest path to LLM observability is usually structured logs plus one dashboard, not a new platform.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Build or buy?<\/h2>\n\n\n<p class=\"wp-block-paragraph\">Start with structured logs and one dashboard. That covers cost attribution, latency percentiles, and error classes \u2014 the three questions that come up in every review. Reach for a dedicated tracing platform when you have multi-step agents, need prompt-level diffing across versions, or want evaluators wired into the same view as traces.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Whichever route you take, keep the schema yours. If your log fields are generic \u2014 feature, model, tokens, latency, status \u2014 you can swap backends without re-instrumenting, and you can point the same pipeline at a new provider by changing a base URL. That portability is the point: a unified, OpenAI-compatible endpoint means observability data from every model lands in one place, with one schema. <a href=\"https:\/\/qoraapi.com\/\" target=\"_blank\" rel=\"noopener\">qoraapi.com<\/a> is one such relay, exposing many models behind a single OpenAI-compatible API.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Frequently asked questions<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">What is the difference between LLM observability and LLM monitoring?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Monitoring watches numbers you already decided matter \u2014 error rate, uptime, spend. Observability is the ability to ask new questions of the raw data, such as why one feature costs five times more than another. Monitoring is a dashboard; observability is the trace and log detail behind it.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How do I track token usage if I stream responses?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Count what you can measure directly: measure TTFT from the first chunk and total duration at the end, and record usage when the stream emits its final usage block. If your endpoint does not return usage on streamed responses, estimate with a tokenizer and reconcile against the provider&#8217;s monthly usage report to catch drift.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Do I need a third-party observability platform?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Not to start. A structured JSON log line per call, shipped to whatever log store you already run, plus one dashboard panel for spend-by-feature, answers most questions. Adopt a platform when you have multi-step traces or need prompt versioning and evaluation in the same view.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How do I attribute cost to individual users?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Tag every call with your own user or API key identifier, then aggregate tokens by that tag and multiply by your price table. This is also your best abuse detector: a single key whose share of total tokens jumps overnight is usually a leaked credential or an infinite loop, not organic growth.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">LLM observability comes down to one discipline: log every call with the same five fields, keep prices in one updatable table, and chart cost and latency by feature rather than in aggregate. Do that and you gain the ability to explain your AI bill instead of merely paying it \u2014 plus an early-warning system for the prompt changes, context leaks, and rate limits that turn a healthy product into an expensive one.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Start with the log wrapper above, add the two dashboard panels that matter most, and read the <a href=\"https:\/\/qoraapi.com\/blog\/reduce-ai-api-costs\/\">cost reduction guide<\/a> alongside the <a href=\"https:\/\/qoraapi.com\/blog\/ai-api-rate-limits-429-errors\/\">429 handling guide<\/a> once your data starts pointing at a problem.<\/p>\n\n\n\n\n<h3 class=\"wp-block-heading\">Related reading<\/h3>\n\n\n<ul class=\"wp-block-list\"><li><a href=\"https:\/\/qoraapi.com\/blog\/evaluate-benchmark-ai-models\/\">Evaluating and Benchmarking AI Models Before You Ship<\/a><\/li><li><a href=\"https:\/\/qoraapi.com\/blog\/ai-usage-metering-billing\/\">Metering and Billing AI Usage Per User: A Practical SaaS Guide<\/a><\/li><li><a href=\"https:\/\/qoraapi.com\/blog\/load-testing-llm-apps\/\">Load Testing LLM Apps: Throughput, TTFT, and Concurrency<\/a><\/li><li><a href=\"https:\/\/qoraapi.com\/blog\/prompt-management-versioning\/\">Prompt Management and Versioning in Production<\/a><\/li><li><a href=\"https:\/\/qoraapi.com\/blog\/text-to-sql-ai\/\">Text-to-SQL: Letting Users Query Your Database with AI<\/a><\/li><li><a href=\"https:\/\/qoraapi.com\/blog\/vector-database-selection\/\">How to Choose a Vector Database for RAG<\/a><\/li><li><a href=\"https:\/\/qoraapi.com\/blog\/context-window-management\/\">Managing the Context Window: Truncation, Summarization, and Sliding Windows<\/a><\/li><\/ul>\n\n","protected":false},"excerpt":{"rendered":"<p>LLM observability for AI APIs: the five signals worth capturing on every call, how to track token usage and cost per feature, why TTFT matters more than average latency, and the alerts that catch runaway spend.<\/p>\n","protected":false},"author":1,"featured_media":100,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[5,6,9,7],"class_list":["post-102","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai-api","tag-ai-api","tag-api-gateway","tag-developer-tools","tag-developers"],"_links":{"self":[{"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/posts\/102","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/comments?post=102"}],"version-history":[{"count":3,"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/posts\/102\/revisions"}],"predecessor-version":[{"id":257,"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/posts\/102\/revisions\/257"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/media\/100"}],"wp:attachment":[{"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/media?parent=102"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/categories?post=102"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/tags?post=102"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}