Performance
Symbolication sits on the ingest path, so the question that matters is what it costs a report and what happens when it can't do its job. Two properties cover most of it: a resolver is built once per build artifact, and a failed build never blocks ingest.
What a lookup costs
The expensive half of symbolication is turning raw debug info (a source map, a DWARF file, an R8 mapping) into a resolver. That happens once per build artifact, not once per exception. The cheap half is the lookup itself: the compiled artifact is a sorted table, so resolving a frame is a binary search, and resolving a 50-frame trace is 50 of them.
Between the two sits the cache. The first exception referencing a new build pays a storage read and a compile. Every exception after that is served from memory, or from a compiled .tw artifact that another instance already built and wrote back to object storage. Concurrent exceptions for the same build don't stampede: the first request builds and the rest wait on it.
Two limits are worth knowing because they are visible in stored traces:
- 50 resolved frames per trace. Deeper frames are stored in the form they arrived in. For DWARF-based formats that count includes inline expansions, so a deeply inlined trace reaches it sooner.
- 5 seconds per store read. A read that exceeds it is treated as a failure, and the trace is stored unsymbolicated rather than held up.
When loading fails
If an artifact was never uploaded, a read times out, storage errors, or a file fails to parse:
- The affected frames pass through unchanged, and the trace is stored as-is. Ingest is never blocked beyond the per-read budget and never errors because of symbolication.
- The failure is negative-cached per key: while the cooldown is active, frames referencing it skip the storage read entirely. A missing artifact starts at a 1-minute cooldown; a transient failure (timeout, storage error, unparseable input) starts at 15 seconds, so brief hiccups recover fast. Each consecutive failure doubles the cooldown, capped at 15 minutes, so a project that never uploads converges to a few storage reads per hour instead of one per minute.
- The cooldown clears the moment that artifact is uploaded — the upload endpoints invalidate it — or as soon as a load succeeds.
- Within a single trace, a failed artifact is attempted once, not once per frame, so a 50-frame trace cannot stack fifty timeouts.
The practical consequence: uploading a missing artifact fixes the next exception, not the ones already stored. Stored traces are immutable snapshots of what could be resolved when they arrived.
When traces arrive unsymbolicated
Work down this list; the causes are in rough order of how often they turn out to be the answer.
- The project has no Upload Token. It is both the upload credential and the switch the ingest path checks — without one, every trace is stored exactly as it arrived. Generate it on the project's Connection page.
- Nothing was uploaded for that build. Re-run the upload and check the
uploadedcount in the response against the number of files you sent: unrecognized files are skipped silently, so a request can return200with nothing stored. - The identifiers don't match. Every format is addressed by the build's own identity — the debug ID in a bundle, the Mach-O UUID of a dSYM, the build ID in a
.symbolsfile, theproguard_uuidof an R8 mapping. A mapping uploaded under one UUID and reported under another will never meet. Compare the value the crash carries with the one the upload used. - The wrong artifact set. JavaScript function names need the minified bundle uploaded next to the
.map; a Flutter build needs one.symbolsfile per architecture you ship; a fat dSYM covers its slices but a thin one covers only its own. - The upload was rejected. A
401(wrong token),413(request over 250 MB) or503(upload queue saturated, retry) in a release job is easy to miss when the job doesn't check the status code. - The trace was already past the 50-frame cap, or the exception isn't in a form the language's parser recognizes. The per-language pages describe the shapes each parser accepts.
An upload takes effect immediately for exceptions that arrive after it, so the fastest verification is to upload, trigger the error again, and look at the new event rather than the old one.
What TracePath watches
The service tracks symbolication health per instance: cache hits, misses and evictions, how many lookups found no uploaded artifact, how many were short-circuited by a cooldown, how long the most recent resolver build took, and how many builds failed outright. A build failure means a trace was stored unsymbolicated, so it is reported into TracePath's own monitoring rather than only counted.
None of that is per-tenant configuration — there is nothing to tune from the dashboard. What it buys you is that a storage or artifact problem on our side is visible to us before it looks like a symbolication problem on yours.
Related pages
- Architecture: the cache tiers and the
.twformat - JavaScript, Dart, iOS, Android: what each format is addressed by, and how to upload it