The default.pgo committed beside your Go service is three releases old. Is the build still correct, and does the profile still help?
answer
- it can only lose value
- no build error tells you it decayed
- matched by function name
- renames and splits break the link
- same commit, built off versus auto
basics
~20 sThe build stays correct: a stale profile can only lose value, never produce wrong code. It degrades gradually as functions are renamed, split or deleted and their samples stop matching. Confirm the remaining benefit by building the same commit with -pgo=off and -pgo=auto and comparing under the same traffic.
solid answer
~50 sCorrectness is not at risk. A Go profile is a hint about where time was spent; if the code has moved on, the compiler simply finds fewer matches and optimises fewer call sites, so the worst case is a build no better than `-pgo=off`. Matching is deliberately tolerant of drift — samples are attributed by function name, and positions within a function are handled relative to the function's start, so a function that merely moved down the file still matches. What genuinely breaks the link is renaming, splitting, deleting or restructuring the hot functions, which is exactly what three releases of refactoring does. The measurable question is whether the profile still earns its keep: build the same commit twice, once `-pgo=off` and once `-pgo=auto`, and compare the two binaries under the same traffic shape. If the gap has closed, refresh the profile from current production rather than debating it.
code
text · 10 lines# how old is the committed profile?
$ git log -1 --format=%ci -- cmd/api/default.pgo
2025-11-03 09:14:02 +0000
# same commit, two binaries: baseline and profile-guided
$ go build -pgo=off -o api-baseline ./cmd/api
$ go build -pgo=auto -o api-pgo ./cmd/api
# refresh: merge captures from several instances into the committed profile
$ go tool pprof -proto inst-a.pprof inst-b.pprof > cmd/api/default.pgogo deeper
The key fact to hold: an out-of-date profile is safe. The compiler ignores what it cannot match, so the build is correct and simply loses some of the speedup.
Explain the matching model — samples attributed by function name, positions relative to the function start — and use it to say which edits are tolerated and which ones sever the link.
Demonstrate the measurement: same commit built with -pgo=off and -pgo=auto, compared under one realistic traffic shape, plus checking when the file was last refreshed. Then say how you would refresh it and why merging captures matters.
Turn the incident into policy. A profile that rotted for three releases is a missing owner and a missing cadence, not a bad afternoon, and you should be ready to say where the refresh step belongs in the release pipeline.
## First, the safety question A stale profile is a *performance* problem, never a *correctness* problem. Nothing the compiler does under PGO changes program semantics: inlining a callee, or emitting a guarded direct call in place of an indirect one, produces code that behaves identically to the unoptimised form. If the profile describes a call site that no longer exists, the compiler finds no match and generates exactly what it would have generated with `-pgo=off`. There is no build error, no warning that fails CI, and no silent miscompilation — which is precisely why staleness is easy to let rot for three releases. ## How gracefully it degrades Go's PGO matching is designed for source drift, because a profile is always captured from a build that is at least one deploy old. Samples are attributed to functions by name, and positions inside a function are handled relative to the start of that function rather than as absolute file line numbers. The practical consequence is that edits *around* a hot function — adding an import, inserting an unrelated function above it, reformatting a neighbouring block — do not invalidate its profile data. What does break the link is change *to* the hot code itself: - a hot function renamed, or moved to a different package, - a hot function split into two, so the samples name a function that no longer contains the work, - a hot path replaced outright — a database query rewritten, a serialization step swapped, - an interface implementation replaced, so the concrete type the profile saw dominating a call site is no longer the one that dominates. That last one is the quiet failure mode of a service whose handler reads an id from the request path and queries a store: swap the store implementation and the devirtualization the profile paid for now guards on a type that never appears, so every hot call takes the fallback path and you carry the guard's cost for nothing. Because the decay is gradual and invisible, the honest description of a three-release-old profile is: probably still delivering part of its original win, on whichever hot functions survived unchanged, and delivering nothing on the parts that were rewritten. ## How to find out, rather than argue about it The measurement is an A/B build, and it is cheap because both halves come from the same commit: 1. Build the current commit twice — `go build -pgo=off -o api-baseline ./cmd/api` and `go build -pgo=auto -o api-pgo ./cmd/api`. 2. Run both against the same traffic shape. For a service the meaningful comparison is CPU per request or CPU at a fixed request rate, taken over a window long enough to cover the real mix of endpoints, not a single synthetic hot loop. 3. Compare against the win the profile was originally adopted for. If PGO once bought a few percent and now buys nothing measurable, the profile has decayed rather than PGO having stopped working. Alongside that, look at the profile's age directly: `git log -1 --format=%ci -- cmd/api/default.pgo` tells you when it was last refreshed, and the answer is usually more damning than any measurement. A profile that predates a major refactor of the hot path should simply be replaced. ## Refreshing Refreshing means capturing a CPU profile from the currently deployed build under real traffic and committing it in place of the old one. Two details matter. First, capture from a workload that represents the mix you care about, and merge several captures if one instance is not representative: `go tool pprof -proto a.pprof b.pprof > cmd/api/default.pgo` writes a combined profile. Second, expect the refresh commit to cause a wide rebuild — the profile influences code generation for dependencies too, so changing it invalidates cached package builds even though no `.go` file changed. ## The iteration worth mentioning Refreshing has a mild feedback property that is worth naming in an interview: the profile you capture next comes from a binary that was itself built with the previous profile, so hot paths that PGO already made cheaper appear slightly less hot in the new samples. In practice this settles rather than oscillates, and it is not a reason to avoid refreshing. It is a reason to refresh on a regular cadence from real traffic instead of chasing a single perfect capture. ## What a good answer sounds like "Nothing is broken — a stale profile can only lose value. It has probably decayed on whatever we rewrote, because samples are matched by function name. I would check when the file was last touched, do an off-versus-auto build of the current commit under the same traffic to see what it is still worth, and then refresh it from production and put a refresh step in the release pipeline so we are not having this conversation again next quarter."
- What kind of source change actually destroys a hot function's profile data?Renaming it, moving it to another package, splitting it so the work lives in a new function, or replacing the hot path outright. Edits around it are tolerated, because samples are attributed by function name and positions are handled relative to the function's start rather than as absolute file lines.
- Why is there no build failure when the profile no longer matches?Because the profile is a hint, not a contract. Unmatched entries simply yield no optimisation decision, so the affected code compiles exactly as it would with `-pgo=off`. Making it an error would break every build whose source has advanced past the profile, which is essentially all of them.
- The refreshed profile comes from a binary that was itself built with the old profile. Is that a problem?It is a mild feedback effect: paths the previous profile already sped up look slightly less hot in the new samples. In practice it converges rather than oscillates. The mitigation is a regular refresh cadence from real production traffic rather than treating any single capture as authoritative.
- Why does refreshing the profile trigger a large rebuild?The profile influences code generation for the whole build, dependencies and standard library included, so its content is part of what the build cache keys on. Changing the file invalidates those cached compilations even though no Go source changed, which is why a profile-refresh commit takes longer to build than its diff suggests.
saying these in an interview costs you the question
- Says a stale profile can produce incorrect code
- Expects the toolchain to warn or fail on staleness
- Thinks any edit to the file invalidates the whole profile
- Judges the profile by reading it instead of measuring builds
- Refreshes from a load test rather than real traffic