How does GODEBUG=inittrace=1 explain seconds of dead time before a Go program's first log line?
answer
- the time before main is still your program
- one line per package, not per function
- offset, clock, bytes, allocs
- a package with no init work prints nothing
basics
~20 sGODEBUG=inittrace=1 makes the runtime print one line per package that has init work, giving that package's start offset, wall-clock duration, bytes allocated and allocation count. Slow package init shows up there, before main runs and before any application logging exists.
solid answer
~50 sPackage-level variable initialisers and `init` functions all run before `main`, so time spent there is invisible to the application's own logs and to any profile started inside `main`. `GODEBUG=inittrace=1` makes the runtime emit one line to standard error for each package that has init work, in the order the linker runs them: the package path, the offset from process start when its init began, the wall-clock time its own init functions took, and the bytes and allocations it made. Packages with no init work print nothing at all, so silence is not zero cost — it means no work. Reading down the lines, the dead time usually collapses onto one package doing something it should not: parsing a config file, building a large table, or making a network call in `init`. The fix is normally to move that work into an explicit, lazily triggered path.
code
text · 5 lines$ GODEBUG=inittrace=1 ./nightly-import -config /etc/import.toml 2>init.log
init internal/godebug @0.28 ms, 0.010 ms clock, 96 bytes, 3 allocs
init time @1.4 ms, 0.062 ms clock, 800 bytes, 12 allocs
init example.com/import/config @6.1 ms, 4180 ms clock, 96468992 bytes, 812004 allocsgo deeper
Know that Go runs package-level variable initialisers and init functions before main, and that GODEBUG=inittrace=1 prints one line per package doing that work.
Be able to read the four columns — start offset, wall clock, bytes, allocations — say that they describe one package's own init rather than its dependencies', and explain why a package with no init work prints nothing.
Close out a start-up latency complaint with it: the dead time is before main, so no application log or in-main profile can see it, and the real fix is usually moving config parsing or network calls out of init into a lazily triggered path.
Own the standing rule about what may run in init at all. Work at package scope has no context, no error path and no logger, which is why start-up cost becomes untraceable by the service's own instrumentation.
## The gap the tool exists to fill A Go program's start-up has a phase your instrumentation cannot see. Package-level variables are initialised and every `init` function runs before `main` is called, in an order the linker computes from the import graph: dependencies first, then the packages that import them. If that phase takes eight seconds, the application's first log line appears eight seconds late, no application log records why, and a CPU profile started at the top of `main` begins after the cost has already been paid. From the outside it looks like the process hung. `GODEBUG=inittrace=1` is the runtime's answer. Set it in the environment at launch — no rebuild, no code change — and the runtime prints one line on standard error for each package that has init work. ## Reading a line The format is: ``` init <package path> @<offset> ms, <clock> ms clock, <bytes> bytes, <allocs> allocs ``` - **package path** — the package whose init work this line describes. - **@offset ms** — how long after process start that package's init began. Reading the offsets down the list tells you where in the sequence the time went. - **clock ms** — wall-clock time spent in that package's own init functions. Because dependencies are separate entries earlier in the list, this number is that package's own work, not a cumulative subtree total. - **bytes / allocs** — heap bytes and allocation count attributed to that init. The format is documented as subject to change; read it, do not build a parser on it. ## What it deliberately does not print The runtime prints nothing for a package that has neither user-written nor compiler-generated init work, and nothing for inits performed as part of plugin loading. So the absence of a line for a package you expected does not mean its init was fast — it means there was no init work to do, or the package was not linked into this build at all because nothing reachable imports it. There is a second, subtler blind spot. The allocation counters are attributed only to the goroutine that runs init. If a package's `init` starts a goroutine that allocates heavily, none of that shows up in the `bytes` or `allocs` columns, and the `clock` column stops when `init` returns even though the goroutine it spawned is still running. A profile of long dead time where every line reports small numbers is a hint to look for exactly this — or for work that has been deferred into a background goroutine which `main` then waits on. ## What the typical culprit turns out to be In a batch job launched by a scheduler, the usual finding is a package that reads and parses a configuration file at package scope, or builds a large lookup table from it, or — worst — dials a remote service in `init`. All three make start-up cost proportional to something outside the program's control, and none of them can be logged, retried, timed out or reported on, because there is no `context.Context`, no logger and no error path in `init`: the only way to fail is to `panic`, which kills the process before anything is running to say why. The usual fix is not to make init faster, it is to stop doing the work there. Move it behind an explicit constructor the program calls from `main`, or behind a `sync.Once`-guarded lazy accessor. Then it is timeable, cancellable, loggable, and testable, and `inittrace` goes quiet because there is no init work left to report. ## Cost of turning it on `inittrace` is not entirely free: it puts the allocator on its accounting path so allocations on the init goroutine can be counted, and the runtime takes a timestamp around each package's init. The overhead is small and bounded to start-up, but it is a reason to treat this as a setting for a deliberate diagnostic run rather than a permanent fixture — which matches the rest of the GODEBUG family, whose output is unstructured stderr text aimed at a human. ## What good looks like in an answer A strong answer connects three facts: init runs before `main`, so nothing inside `main` can observe it; `inittrace` is set in the environment, so no rebuild is needed on a binary you cannot rebuild; and the lines are per package with the offsets telling you where the time went. A weaker answer reaches for a profiler or adds print statements to `main`, both of which arrive after the interesting part is over.
- A package's init starts a goroutine that allocates heavily. Does inittrace's bytes column include it?No. The runtime attributes init allocations only to the goroutine running init, so work handed to another goroutine is missing from the bytes and allocs columns, and the clock column stops when init returns even though that goroutine keeps running. Long dead time with small numbers on every line points at exactly that.
- inittrace prints no line for a package you know has an init function. What does that mean?Most likely that package is not in this build. The runtime prints a line for every package that has init work, so a package that is linked in and runs init will appear. Nothing is printed for packages with no user-written or compiler-generated init work, and nothing for inits run during plugin loading.
- Is enabling inittrace free?Nearly, but not quite. It switches the allocator onto its accounting path so allocations on the init goroutine are counted, and the runtime timestamps each package's init. The cost is small and confined to start-up, but it is a reason to set it for a diagnostic run rather than leaving it on permanently.
saying these in an interview costs you the question
- Thinks start-up cost can only come from code inside main
- Expects one line per imported package regardless of init work
- Reads the clock column as including dependencies' init time
- Assumes goroutines started in init are counted in the bytes column
- Proposes profiling start-up with a CPU profile started in main