#12758·zitadel

Login V2: getNodeAutoInstrumentations() loads 45 OpenTelemetry packages on the cold start path

Author: TheLevtiCreated Sep 16, 2026Updated Sep 16, 2026

Summary

apps/login/src/instrumentation.node.ts builds its instrumentation list with getNodeAutoInstrumentations(). That call require()s 40 instrumentation packages and 5 cloud resource detectors at module load, and Next.js awaits register() before serving the first request, so the whole set sits on the cold start path between the port binding and the server answering.

The login app's own dependency surface is much narrower than that set, so most of the loaded instrumentations can never match anything.

Measurement

v4.16.2 login image, container start to first HTTP response, interleaved A/B between the unchanged image and one importing only the instrumentations the app uses. Two CPU shapes:

CPU Unchanged Narrowed Delta
0.5 1.64s mean (n=7) 1.46s mean (n=7) 0.18s, narrowed faster in 7 of 7 paired rounds
0.1 (throttled) 21.0s mean (n=3) 17.7s mean (n=3) ~3s

For reference, OTEL_SDK_DISABLED=true on the same 0.1 CPU harness measured 13.3s, so the SDK as a whole is roughly 8s there and this accounts for a little under half of it.

The 0.5 figure is the honest one for a normally-scheduled container; 0.1 is a deliberately throttled worst case we use to reproduce probe failures.

Why the environment variables do not solve it

OTEL_NODE_ENABLED_INSTRUMENTATIONS and OTEL_NODE_DISABLED_INSTRUMENTATIONS look like the intended escape hatch, but in @opentelemetry/[email protected] they are read inside getNodeAutoInstrumentations(), after build/src/utils.js has already require()d every instrumentation package and every resource detector at module top level. They therefore skip patching, never loading, so an operator who sets them still pays the full module-load cost. That may be worth documenting regardless of what you decide here.

What the login app appears to use

From apps/login's dependencies and call sites:

  • http - Next.js serves over it, and src/lib/zitadel.ts reaches the ZITADEL API via createConnectTransport({ httpVersion: "1.1" }), i.e. Connect over HTTP/1.1, which instrumentation-http already covers. (nice-grpc is in the manifest but has no call site.)
  • undici - global fetch, used directly in src/lib/server/security-settings.ts and internally by Next.js.
  • winston - the existing config already sets disableLogSending: false.
  • runtime-node - Node event-loop and heap metrics.

We also found that instrumentation-dns and instrumentation-net are worth calling out separately: unlike the rest of the bundle they patch Node built-ins rather than app dependencies, so they do emit here. Narrowing loses their socket-level spans, which is a real if small trade rather than a no-op.

Possible directions

  1. Import the four (or five, with dns/net) instrumentations directly instead of the auto bundle.
  2. Keep getNodeAutoInstrumentations() but document that the OTEL_NODE_*_INSTRUMENTATIONS variables cannot recover the load cost.
  3. Leave as is, if broad coverage for self-hosters who extend the login app is the deliberate intent.

Happy to open a PR for (1) if that is a direction you would take. We are currently carrying (1) as a local change in our fork, and would rather converge than diverge if you are open to it.

Environment