Errors
Find the exact string you saw. Every entry says what produced it and what to do.
The SDKs are fail-open by design: capture problems are logged and the application keeps running. That is the correct trade, and it is also why a misconfiguration can go unnoticed. Read your logs once after wiring Metergraph up.
Where to look
Section titled “Where to look”| Language | Where messages go |
|---|---|
| Python | The metergraph logger, at WARNING. Nothing is printed unless logging is configured |
| TypeScript | console.warn and console.info |
Repeated transport failures are rate-limited. The first occurrence of each kind
is logged immediately, further ones inside a 60 second window are suppressed, and
the next line for that kind carries (N more suppressed in the last 60s). So a
single warning can represent a great many failures.
HTTP status codes
Section titled “HTTP status codes”Returned by the Ingest API.
400 Bad Request
Section titled “400 Bad Request”The batch will never succeed. Drop it.
| Cause | Fix |
|---|---|
More than 5,000 rows in rows | Split the batch. POST /v1/traces returns 413 for this instead |
rows is missing, empty, or not a list | Send at least one row |
| A row is not an object | Every entry in rows must be a JSON object |
| The body is not JSON, or not a JSON object | Check Content-Type and the serializer |
schema_version is not an integer, or is not 1 | Send 1, or omit it |
meta is present but not an object | Send an object, or omit it |
Content-Encoding: gzip on a body that is not valid gzip | Compress the body, or drop the header |
| An OTLP body that will not decode in its declared encoding | Match Content-Type to what you actually sent |
401 Unauthorized
Section titled “401 Unauthorized”| Cause | Fix |
|---|---|
No Authorization header, or not a Bearer header | Send Authorization: Bearer <token> |
| Unknown or revoked ingest key | Mint a new one in Ingest keys |
Expired or invalid mgs_ session token | Re-exchange at POST /v1/ingest/sessions. The SDKs do this automatically |
| The workspace is inactive or being purged | Contact the workspace owner |
402 Payment Required
Section titled “402 Payment Required”quota_exceeded. The monthly captured-call allowance is spent. The response body
carries the counter, the allowance and the period.
Capture resumes at the start of the next UTC month, or when the allowance is raised. See Limits and allowances.
403 Forbidden
Section titled “403 Forbidden”The token resolved, but it lacks the scope the endpoint needs. Every ingest
endpoint needs ingest.
A dashboard-created key always carries ingest, so a 403 here usually means
you are using a read or agent:read key minted by the operator CLI. Mint an
ingest key instead.
413 Payload Too Large
Section titled “413 Payload Too Large”| Cause | Fix |
|---|---|
| Body over 8 MiB on the wire | Split the batch in half and retry each half, down to a single row |
| Body over 8 MiB after gzip decompression | The same. The limit is checked twice |
More than 5,000 GenAI spans on POST /v1/traces | Reduce the export batch size in your OTel BatchSpanProcessor |
415 Unsupported Media Type
Section titled “415 Unsupported Media Type”| Cause | Fix |
|---|---|
A Content-Encoding other than identity or gzip | Send one of those two, or omit the header |
A Content-Type on POST /v1/traces that is not application/json or application/x-protobuf | Set one of those two |
422 Unprocessable Entity
Section titled “422 Unprocessable Entity”Only from POST /v1/ingest/sessions.
| Cause | Fix |
|---|---|
protocol_version is not exactly 2 | Send 2 |
repository is missing, empty, or not a string | Send owner/name |
sdk_version is missing or empty | Send your SDK version string |
| The repository identifier was rejected | Match owner/name: one slash, no spaces |
503 Service Unavailable
Section titled “503 Service Unavailable”Only from GET /healthz, when the database check failed. Nothing to do on your
side.
Setup and initialization messages
Section titled “Setup and initialization messages”Metergraph capture disabled: token and ingest URL are required
Section titled “Metergraph capture disabled: token and ingest URL are required”No token was found. init() did not mark itself initialized, so a later init()
that supplies one will still work.
Fix. Set METERGRAPH_APP_TOKEN in the process that makes the LLM calls. Not
your shell, not the build step, not a .env file the runtime never reads. Mint a
key in Ingest keys.
Logged once per process.
Metergraph init() was called more than once; the first configuration remains active.
Section titled “Metergraph init() was called more than once; the first configuration remains active.”A second init() was ignored. Every later call is a no-op.
The usual cause is calling wrap() before your own init(...). wrap() calls
init() for you, so by the time your configured init() runs, the
environment-only configuration is already locked in.
Fix. Call init(...) first, at the top of your entry point, before anything
imports or wraps a client.
Metergraph initialization failed; application is running uninstrumented
Section titled “Metergraph initialization failed; application is running uninstrumented”Something raised inside init(). The guard caught it, cleared the runtime, and
left the process uninstrumented.
The most common cause is a numeric environment variable that is not a number.
Python parses METERGRAPH_QUEUE_SIZE, METERGRAPH_BATCH_SIZE,
METERGRAPH_FLUSH_SECONDS, METERGRAPH_TEXT_MAX_BYTES,
METERGRAPH_CONFIG_POLL_SECONDS and METERGRAPH_CONFIG_HARD_TTL_SECONDS inside
this guard, and a value like 5s or 1 MiB raises.
Fix. Check every METERGRAPH_* variable holds a bare number. See
Configuration.
Metergraph repository identity is not configured
Section titled “Metergraph repository identity is not configured”Full text, Python:
Metergraph repository identity is not configured; setinit(repository='owner/repository'), METERGRAPH_REPOSITORY, or provide.metergraph/config.json. Continuing with legacy ingestion.Capture still works. What you lose is repository attribution, and the SDK falls
back to sending the app token directly to /v1/ingest rather than exchanging it
for a short-lived session token.
Fix. Set METERGRAPH_REPOSITORY=owner/name, or pass repository= to
init(), or commit a .metergraph/config.json. Logged once per process.
metergraph: found <path> but could not read it
Section titled “metergraph: found <path> but could not read it”The .metergraph/config.json exists but is unreadable or is not valid JSON.
Fix. Check permissions and the JSON syntax.
metergraph: <path> has an unsupported schema version; ignoring (expected version 2)
Section titled “metergraph: <path> has an unsupported schema version; ignoring (expected version 2)”The config file declares a version other than 2.
Fix. Set "version": 2, or omit the field entirely.
metergraph: <path> is missing a valid ‘repository’ field; ignoring
Section titled “metergraph: <path> is missing a valid ‘repository’ field; ignoring”The config file has no repository, or it does not contain a slash.
Fix. Set "repository": "owner/name".
Wrapping messages
Section titled “Wrapping messages”Metergraph found no supported methods on <label> client
Section titled “Metergraph found no supported methods on <label> client”wrap() ran but recognized nothing to patch. The client is returned unmodified.
| Cause | Fix |
|---|---|
| Not a supported client | Only OpenAI, Anthropic and Google clients are supported. See the seam tables for Python and TypeScript |
| A provider SDK version whose method names moved | Upgrade the Metergraph SDK |
| You wrapped a sub-resource instead of the client | Wrap the client object itself, not client.chat |
Metergraph patched N seam(s) on <label> client: …
Section titled “Metergraph patched N seam(s) on <label> client: …”Informational, at INFO. This is what success looks like. (batch-only) means
only Batch API result readers were patched, which is correct for a client used
solely for batch submission.
Metergraph wrap() failed; client is unmodified and uninstrumented
Section titled “Metergraph wrap() failed; client is unmodified and uninstrumented”An unexpected error inside wrap(). Capture is off for this client and your
application is otherwise unaffected. Python logs the traceback, TypeScript
attaches the error object.
Fix. Report it with the traceback and your provider SDK version.
metergraph.wrap() received an unsupported gateway; supported gateways are: openrouter
Section titled “metergraph.wrap() received an unsupported gateway; supported gateways are: openrouter”Raised, not logged. ValueError in Python, Error in TypeScript, before any
provider call. Your value is never echoed back, in case it is a secret.
Fix. The only supported gateway is openrouter. For the Vercel AI Gateway,
use provider="vercel" in Python, or
vercelAISDKMiddleware()
in TypeScript.
metergraph.wrap() gateway requires an OpenAI-compatible provider
Section titled “metergraph.wrap() gateway requires an OpenAI-compatible provider”You combined gateway= with a provider= other than "openai". A gateway
override requires the OpenAI response contract.
Fix. Drop the provider=, or set it to "openai".
metergraph.wrap(gateway=…) requires an OpenAI-compatible client
Section titled “metergraph.wrap(gateway=…) requires an OpenAI-compatible client”You passed gateway= to a client that detects as Anthropic or Google.
Fix. Use an OpenAI-compatible client for a gateway call.
aiSdkVersion and specificationVersion cannot be combined
Section titled “aiSdkVersion and specificationVersion cannot be combined”TypeScript only. TypeError from vercelAISDKMiddleware().
Fix. Pass aiSdkVersion: 5 on AI SDK 5, or specificationVersion on AI SDK
6 and 7. Never both.
track requires a function
Section titled “track requires a function”TypeScript only. TypeError. You called track("name") without a second
argument.
Fix. track(fn) or track("stable.name", fn).
modelFor requires options.default to be a non-empty string
Section titled “modelFor requires options.default to be a non-empty string”TypeScript only. TypeError.
Fix. Pass a non-empty default. This is the one function in the SDK that
throws on bad input rather than degrading.
Context scope messages
Section titled “Context scope messages”set_session() requires an active Metergraph context
Section titled “set_session() requires an active Metergraph context”Full text, Python:
metergraph.set_session() requires an active Metergraph context; usemetergraph.context() or metergraph.session().TypeScript:
Metergraph setSession() requires an active Metergraph context; usewithSession() or withContext().You called the mutating form outside any Metergraph scope. There was nothing to mutate, so it did nothing.
Fix. Either enter a scope first, or use the scoped form directly.
# Does nothing at module level:metergraph.set_session(user_id)
# Works:with metergraph.session(user_id): ...
# Also works: set_session() inside an existing scopewith metergraph.route("checkout.summarize"): metergraph.set_session(user_id)Logged once per process.
set_tags() requires an active Metergraph context
Section titled “set_tags() requires an active Metergraph context”The same rule, naming set_tags() and tags() in Python, setTags() and
withTags() in TypeScript.
Transport messages
Section titled “Transport messages”All of these carry the metergraph: prefix and are rate-limited.
Metergraph authentication failed; capture disabled for this process
Section titled “Metergraph authentication failed; capture disabled for this process”The ingest endpoint returned 401 or 403 for an app token. Capture stops
for the life of the process, because retrying a rejected credential is pointless.
Fix. Check the token. Mint a new key if it was revoked. Restart the process after fixing it: this state is not recoverable in place.
With a session token this message does not appear. The SDK invalidates the session and re-exchanges instead.
ingest rejected batch with HTTP <code> against <url>; dropping this batch (payload-specific, not a process-wide failure)
Section titled “ingest rejected batch with HTTP <code> against <url>; dropping this batch (payload-specific, not a process-wide failure)”A 400, 404, 413 or 422. The batch is dropped and capture continues.
| Code | Likely cause |
|---|---|
400 | A malformed row got into the queue. Usually a custom client |
404 | The ingest URL is wrong. Check METERGRAPH_INGEST_URL for a stray path |
413 | A single row over 4 MiB, after the SDK’s split-and-retry gave up |
422 | A rejected session exchange |
ingest request failed with HTTP <code> against <url>
Section titled “ingest request failed with HTTP <code> against <url>”Any other non-202, usually a 5xx or a 402. The SDK backs off from 1 second,
doubling to a 60 second ceiling, and drops rows while it waits.
A 402 here means the monthly allowance is spent. See
402 Payment Required.
ingest request to <url> failed: <Type>: <message>
Section titled “ingest request to <url> failed: <Type>: <message>”A network-level failure: DNS, TLS, timeout, connection refused.
| Cause | Fix |
|---|---|
| Wrong or unreachable ingest URL | curl the URL’s /healthz |
| Egress blocked by a firewall or proxy | Allow the ingest host |
| Timeout under load | The SDK’s request timeout is 5 seconds. Sustained timeouts mean a network problem, not a Metergraph one |
session exchange to <url> failed with HTTP <code>
Section titled “session exchange to <url> failed with HTTP <code>”The app token could not be exchanged for a session token. Rows are dropped rather than sent with the app token. Backoff doubles from 1 second to 60.
| Code | Cause |
|---|---|
401 | Unknown or revoked app token |
403 | The key lacks the ingest scope |
422 | The repository string is not owner/name |
session exchange to <url> returned no session_token
Section titled “session exchange to <url> returned no session_token”The endpoint answered but the body had no session_token. Almost always a proxy
rewriting the response.
Fix. Check anything sitting between your process and the ingest host.
config poll to <url> failed with HTTP <code>
Section titled “config poll to <url> failed with HTTP <code>”The GET /v1/config poll failed. Harmless: model_for() returns your default.
Metergraph config authentication failed; using default models
Section titled “Metergraph config authentication failed; using default models”GET /v1/config returned 401 or 403. The poller stops permanently for
this process. Capture is unaffected.
Fix. Nothing is required. If you want the poller back, fix the key and restart.
skipped an eligible GenAI span with no model attribute
Section titled “skipped an eligible GenAI span with no model attribute”Python OTLP exporter only, rate-limited. A span looked like a GenAI span but carried no model attribute, so no row could be built. The message names the instrumentation scope and span kind and no attribute values.
Fix. Set the model attribute in your instrumentation, or exclude that scope
with exclude_scopes. See
the exporter reference.
Batch-first refusals
Section titled “Batch-first refusals”All raised as BatchFirstIneligibleError, before any provider call, so nothing
has executed when you see one.
| Message fragment | Fix |
|---|---|
requires accept_duplicate_provider_execution=True | Pass it explicitly. A missed deadline runs your request twice and you pay for both |
requires a positive deadline_seconds (Python) or requires a positive policy.deadlineMs (TypeScript) | Pass a positive finite number |
does not support streaming requests | Streaming is direct-only. Drop stream |
requires allow_duplicate_tool_call_plans=True for requests with tools | The two executions may pick different tool plans. Acknowledge it, or drop the tools |
has no adapter for provider "..." | Supported providers are openai, anthropic and google |
request is not eligible for batch-first execution: | The adapter’s own reason follows |
A submission failure is raised directly rather than triggering the fallback, because no batch was ever created.
Analysis run failures
Section titled “Analysis run failures”Shown on Analysis under Recent attempt.
| Code | Meaning |
|---|---|
pricing_preflight_failed | A production model in scope has no catalog price. The message names the models |
launch_failed | The analysis service was unavailable. Contact support |
bootstrap_failed | The run could not be set up |
export_failed | Traffic could not be exported |
analyze_failed | The analysis itself failed |
import_failed | The report could not be imported |
abandoned | The run stopped making progress and was closed |
For pricing_preflight_failed, see
diagnosing a total that looks too low.
No rows are arriving
Section titled “No rows are arriving”The most common report, and rarely an error message. Work down this list.
-
Is the token set in the process that makes the calls? Not the shell, not the build step. Unset, the SDK logs one warning and stays silent forever after.
-
Did the process live long enough to send its queue? Batches leave every 5 seconds by default. Call
shutdown()before a short-lived process exits. In serverless code usebindWaitUntil()orwrapHandler(). See Serverless and short-lived jobs. -
Is the client actually wrapped? Assign the return value.
wrap()returns the client, and throwing it away captures nothing.client = metergraph.wrap(OpenAI()) # correctmetergraph.wrap(OpenAI()) # captures nothing -
Are calls going through a path you did not wrap? A second client, a raw HTTP call, a background worker with its own setup, a library that constructs its own client.
-
Is
METERGRAPH_DISABLEDset to1in that environment? Only the exact string1disables the SDK, so check for it precisely. -
Is the ingest URL right? Run the
curlabove. A connection error points at the URL. A404points at a stray path onMETERGRAPH_INGEST_URL, which must be a base URL with no/v1suffix. -
Is the allowance spent? A
402, and a Capture paused banner in the dashboard. -
Has the batch been processed yet? Rows are durable on
202but appear in the dashboard after the worker runs. Give it a minute before concluding anything.
Streaming calls show no token counts
Section titled “Streaming calls show no token counts”OpenAI omits usage from a streaming response unless you ask for it. Metergraph
adds stream_options.include_usage to streaming chat.completions requests that
do not already set it.
Two situations turn that off:
| Situation | Consequence |
|---|---|
METERGRAPH_PATCH_STREAM_USAGE=0 | Streamed chat calls arrive with no token counts, and therefore no cost |
| A detected gateway | The patch is deliberately skipped, because a gateway emits its own usage event |
Fix. Unset METERGRAPH_PATCH_STREAM_USAGE, or set stream_options yourself.
Function names are wrong or missing
Section titled “Function names are wrong or missing”Stack attribution is a fallback. A bundler renames functions and rewrites paths, so built TypeScript output frequently attributes to the wrong name or to nothing.
Fix. Use track("stable.name", fn) on every function that calls a wrapped
client. In Python, use @metergraph.track where the calling frame sits inside a
library rather than your own code.
Two other causes:
- The calling file is outside
app_root. Only frames under that root become attribution frames. - The calling file matches a skip pattern:
site-packages,metergraph/_capture.py,concurrent/futures,threading.py, or anything you added withskip_frames.
Rows go missing under load
Section titled “Rows go missing under load”The queue drops rather than blocking your application. That is the correct trade and it does mean a burst can shed rows.
Fix. Raise METERGRAPH_QUEUE_SIZE. Every ingest request reports its own
running counts in meta.dropped and meta.transport_errors, so the drop is
visible rather than silent.
Context is lost across threads
Section titled “Context is lost across threads”Route, session and tags live in a context variable. Work handed to a thread pool starts with an empty context.
Fix. In Python, wrap the executor:
from concurrent.futures import ThreadPoolExecutorpool = metergraph.wrap_executor(ThreadPoolExecutor())asyncio tasks and async def code need nothing. Neither does TypeScript, which
uses AsyncLocalStorage.
See Attribution across threads.
Content is missing from a trace
Section titled “Content is missing from a trace”The span inspector reads Request not captured or Response not captured.
| Cause | Fix |
|---|---|
METERGRAPH_CAPTURE_TEXT is 0, false, no or off | Unset it, or set 1 |
capture_text=False on init(), or on the enclosing route(), trace() or context() | A narrower scope can only turn content off, never back on |
| You are on the self-hosted OSS server | It has no content column. Content is discarded structurally |
Truncated at the 100 KB field limit is not an error. It is the server-side cap.
See Captured fields.