Source map upload at build time vs runtime: tradeoffs
Build-time upload is faster but couples deploys to your error tracker; runtime fetch is slower but more resilient. Picking one.
You just shipped a critical hotfix to production. Five minutes later, an alert hits Slack. You click the link, ready to debug, only to find a cryptic mess of at a.default (main.b7f2.js:1:4502). The error tracker is still "processing" the source maps you uploaded during the build—or worse, its crawler is being blocked by your staging firewall. The golden hour of debugging, the window where you actually remember what you changed, gets burned on infrastructure troubleshooting instead of the bug.
Almost every team treats source map configuration as a set-it-and-forget-it chore. But the decision you make here—whether your error tracker gets your maps by being pushed them at build time or by pulling them at runtime—quietly shapes your CI/CD reliability, your security posture, and how fast you can read a stack trace at 3 AM. This post walks through both models, where each one breaks, and how to pick.
The De-obfuscation Loop: How Error Trackers Use Source Maps
The mechanics are simple even if the encoding is not. Your bundler (Rollup, esbuild, webpack) takes src/auth.ts and a dozen other files, mangles every identifier, strips whitespace, and stitches everything into one line of main.b7f2.js. Alongside it, the bundler emits a .map file: a JSON sidecar that records, for every position in the generated file, which original file, line, column, and name it came from. When an error fires at line 1, column 4502, the tracker looks up that coordinate in the map and reconstructs AuthService.ts:42 → validateToken(). If we go deeper on the encoding itself, our post on how stack-trace deminification actually works walks through the VLQ math.
The catch is that the tracker can only do this if it has the right map for the exact bundle that threw. There are exactly two ways for it to get there.
The "Push" model (build-time)
During your build, a CLI or bundler plugin uploads each .map file directly to your error tracker, tagged with a release identifier (usually a git SHA). The maps live inside the tracker's storage, version-locked to that release. By the time the bundle is deployed, the tracker already has everything it needs and never has to reach back into your infrastructure.
The "Pull" model (runtime/on-demand)
You deploy the maps next to your JavaScript (or leave the //# sourceMappingURL= comment pointing at them). When an error arrives, the tracker's crawler fetches the referenced .map over HTTP, on demand, the first time it sees a given file. Zero build changes. The map only has to exist somewhere the crawler can reach.
Build-Time Upload: The Proactive Approach
This is the model serious teams converge on, and the reasons cluster into three buckets: security, reliability, and speed.
Security: keeping maps off the public internet
A source map with sourcesContent embedded is your entire original codebase—comments, TODOs, internal API shapes, and all. Build-time upload lets you ship the map to the tracker and then delete it before the bundle hits your CDN. The public never sees it; only the tracker does. We cover the full generate-upload-delete workflow in hiding source maps from end users.
Reliability: version-locking maps to release SHAs
When maps are uploaded under a release SHA, the tracker matches the bundle that threw to the exact map produced by that commit. No ambiguity, no "which deploy was this from" guessing three weeks later. This is the only model that gives you 100% reliable resolution, because the matching key is deterministic rather than a URL that may have changed.
Performance: instant resolution, no crawler wait
Because the map is already sitting in the tracker, the first error resolves immediately. There is no cold fetch, no DNS round-trip to your origin, no waiting for a queue. The trace is readable the instant it lands.
Here is what a build-time upload looks like in a GitHub Action, using a Sentry-compatible CLI:
# .github/workflows/deploy.yml
- name: Build
run: npm run build
- name: Upload source maps
run: |
npx @sentry/cli sourcemaps inject ./dist
npx @sentry/cli sourcemaps upload \
--release "$GITHUB_SHA" \
--url-prefix "~/assets" \
./dist
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SOURCEMAP_TOKEN }}
SENTRY_URL: https://glitchreplay.com
- name: Strip maps before deploy
run: rm -f ./dist/**/*.map
- name: Deploy
run: npm run deployThe Hidden Friction of Build-Time Uploads
Build-time upload is not free, and pretending otherwise is how teams end up resenting it.
CI/CD bloat
Source maps are big. For a typical React app, the maps run roughly three to five times the size of the minified JavaScript—a 2 MB bundle can produce 8–10 MB of maps. Uploading that on every PR preview build adds real seconds, and on a busy monorepo with dozens of entrypoints it compounds. The fix is to scope uploads to deploy branches, not every push.
The API dependency
If your upload step is in the critical path and the ingest API is down, your deploy fails. You have to decide whether a source-map upload failure should block a production release. The sane answer is usually no: mark the step continue-on-error and reconcile later, so a tracker outage never holds your hotfix hostage.
Versioning hell
In a monorepo, matching dist artifacts to the right uploaded release across multiple apps is fiddly. The url-prefix and release values must line up exactly with what the SDK reports at runtime, or the tracker has the map but can't connect it to the error.
Runtime Fetching: The "Lazy" Alternative
The appeal of the pull model is that it asks nothing of your build.
Zero-config debugging
You change nothing in your pipeline. The maps are deployed, the sourceMappingURL comment is present, and the tracker figures it out. Setup is roughly two minutes versus the thirty to sixty minutes a build-time integration takes the first time.
Handling dynamic environments
Ephemeral preview deployments are where runtime fetching genuinely shines. Every PR gets a throwaway URL, and you don't want to upload (and then clean up) maps for environments that exist for an hour. Letting the tracker pull from the preview origin on demand is pragmatic.
Why Runtime Fetching Often Fails in Production
For anything beyond a prototype, the pull model is brittle in ways that only show up during an incident.
Firewall and VPN barriers
Runtime fetching requires giving a third-party crawler network access to fetch assets from your infrastructure. If your assets sit behind a VPN, a WAF rule, or basic auth on staging, the crawler gets a 403 and you get an unresolved trace—exactly when you need it most.
The race condition
The very first user to hit a new error triggers the map fetch. If that fetch times out, hits a cold cache, or races a deploy that just rotated the file hash, that error resolves to garbage. Build-time upload has no first-hit penalty because the map is already there.
Source code leakage
This is the big one. For the tracker to pull your maps, the maps have to be reachable—which usually means public. "Runtime" quietly becomes "publicly hosted source code," and now anyone with curl can download your frontend.
Security Deep Dive: The Threat of Exposed Source Maps
Why does runtime almost always mean public? Because the simplest, zero-config setup is to leave the .map next to the .js on your CDN. That convenience is the vulnerability.
Scrapers and source map walkers
There are off-the-shelf tools that take a single sourceMappingURL and reconstruct your entire original source tree—directory structure, filenames, comments, the lot. One exposed map is a full repo checkout for anyone who wants it. Google Search Central explicitly recommends that source maps not be crawlable, and security scanners routinely flag exposed .map files as a finding.
PII and comments
That // HACK: bypass auth check for the legacy admin until we fix RBAC comment you wrote at midnight? It ships in the map. So does the internal endpoint you commented out, the feature-flag names, and any hardcoded value a developer left in a comment. Minification hides none of this once the map is one GET request away.
The Decision Matrix: Which Should You Choose?
Use runtime fetching when…
Internal-only tools, throwaway prototypes, and ephemeral preview environments where the source isn't sensitive and setup time matters more than rigor. If the code being mapped is open source anyway, the leakage concern evaporates.
Use build-time upload when…
Any customer-facing SaaS product, anything touching regulated data (HIPAA, GDPR, PCI), and any high-traffic app where the first-hit race condition is unacceptable. If your source code is intellectual property, this is the only defensible choice.
The hybrid strategy
The pragmatic middle path: runtime fetching in dev and staging where convenience wins, build-time upload locked down for production. You get fast iteration in low-stakes environments and zero-trust privacy where it counts. To verify production never leaks, our security headers checker and a quick curl -I https://yourapp.com/assets/index.js.map (you want a 404) close the loop.
Optimizing the Build-Time Flow with GlitchReplay
The reason teams avoid build-time upload is the friction, and most of that friction is fixable. GlitchReplay is Sentry-SDK compatible, so the same bundler plugins and @sentry/cli commands you already know point at our endpoint with one config change—no rewrite of your pipeline. Once a map is uploaded under a release, our backend resolves traces instantly on ingest, with no crawler ever touching your infrastructure. If a trace already slipped through unresolved, our free source map deminifier turns it readable in your browser, and our source map validator confirms a map actually lines up with its bundle before you trust it.
And because pricing is flat-rate, you can iterate on your source map setup—triggering errors, re-uploading, re-testing—without watching an event meter tick toward an overage. Stop gambling with your production stack traces. Set up build-time upload once, point it at a tracker that resolves on ingest, and get the readable trace during the golden hour instead of after it. The full configuration details live in our source maps documentation.
GlitchReplay is Sentry-SDK compatible, includes session replay and security signals, and never charges per event. Free to start, five minutes to first event.