Your Telemetry Boots Before Your Env Vars, and That's Why the Traces Vanish
SvelteKit's 3.0 prereleases changed when environment variables are populated relative to instrumentation.server.js. The same ordering trap exists in plain Node: your OpenTelemetry preload runs before dotenv, reads an undefined endpoint, and quietly drops every span. Here is the boot order, why you can't just move the SDK later, and three fixes that actually hold.

SvelteKit's 3.0 prerelease train shipped a changelog line last week that reads like housekeeping:
> populate env vars before instrumentation.server.js is evaluated and update the adapter instrumentation API
It went out in @sveltejs/[email protected], and the adapters were still churning through it in the next.28 batch on September 24. Same batch also started rejecting query params that begin with x-sveltekit-, which is a different story.
I stopped on that env line for longer than it deserved. Not because SvelteKit is my daily driver, it isn't, but because it describes a bug I have personally shipped twice, in plain Node services that have never seen a .svelte file. Both times it presented the same way: the deploy is green, the health check is green, and the tracing dashboard is empty. Nobody notices for a week because nothing is *broken*, there's just nothing there.
The boot order nobody writes down
Here's the setup. You run your server with a preload so the OpenTelemetry SDK can patch http, pg, and friends before anything imports them:
node --import ./instrument.mjs server.jsAnd inside server.js, somewhere near the top, you have the usual:
import "dotenv/config";Read those two in the order the runtime actually executes them.
The preload wins. It always wins, that's the entire point of a preload. So the SDK reads its endpoint, its headers, its service name, and its sampling ratio out of a process.env that your .env file has not touched yet.
Why you can't just move the SDK later
The obvious fix is to load config first and start the SDK afterwards, inside the app. That fix is wrong, and it's wrong for a reason worth internalizing.
Auto instrumentation works by intercepting module loads. It wraps http.request, it wraps the pg client constructor, it wraps whatever else it knows about. If your app has already imported Express and Postgres by the time start() runs, those references point at the unwrapped originals. The SDK comes up healthy, reports no errors, and produces a trace tree with a giant hole where your database calls should be.
So instrumentation genuinely has to be first, and config genuinely has to be before instrumentation. Those two constraints fight, and most frameworks resolve the fight by accident rather than on purpose. SvelteKit just made its answer explicit. Next.js gets there from the other direction, loading .env during server bootstrap before instrumentation.ts calls register(). Bare Node gives you nothing and lets you find out in production.
The failure is silent by design
This is the part that makes it expensive. An undefined OTLP endpoint doesn't throw. The exporter falls back to http://localhost:4318, the batch processor happily accepts spans, and the export attempt fails on a background timer that has no reason to crash your request handler.
Your app is doing all the work of tracing and throwing it in a bin. CPU cost, zero payoff.
Three fixes, in the order I'd reach for them
Stop using dotenv on the server. If you're on a container platform, systemd, or anything with a real env block, the variables should already exist in the process before Node starts. .env files are a local development convenience that quietly became production infrastructure, and I think that was a mistake. This removes the ordering problem instead of sequencing around it.
If you do need a file, use Node's own flag rather than a library:
node --env-file=.env --import ./instrument.mjs server.jsThe runtime applies --env-file while it's still processing startup configuration, so the variables are in process.env before your preload module is evaluated. One flag, no import graph to reason about.
Then, whichever route you take, make the preload refuse to run half configured:
// instrument.mjs
const required = ["OTEL_EXPORTER_OTLP_ENDPOINT", "OTEL_SERVICE_NAME"];
const missing = required.filter((k) => !process.env[k]);
if (missing.length) {
throw new Error(
`instrument.mjs ran before these were set: ${missing.join(", ")}`
);
}
const { NodeSDK } = await import("@opentelemetry/sdk-node");
// ...start the SDKA crash loop on deploy is annoying for twenty minutes. A dashboard that's been lying to you since March is annoying for a quarter.
If you're actually on SvelteKit
Two things to do. First, if you wrote a workaround because instrumentation.server.js couldn't see $env/dynamic/private, such as reaching into process.env by hand or deferring the SDK start until the first request, delete it. The reason it existed is gone.
Second, if you maintain an adapter, this release also replaced builder.generateManifest with builder.generateServerInstance plus builder.manifest, and pulled the Server constructor and SSRManifest out of the public types. Those are all part of the same reshuffle: the framework now owns when your server instance is constructed, which is what let it slot env loading in front of instrumentation in the first place.
The general version
Every framework has a file that runs before everything else, and every framework answers "what can that file see" differently. Almost nobody writes the answer down, including the people who chose it.
Go find yours. Put a console.log(Object.keys(process.env).length) at the top of it, run the server, and see whether the number matches what you assumed. Takes thirty seconds, and it's cheaper than the alternative, which is a very confident incident review about a trace you never actually collected.
Sources: sveltejs/kit releases, SvelteKit September 2026 updates