#10334·pipeline

Step termination messages accumulate all prior results, breaking per-step 4KB budget (and the docs advice)

Author: vdemeesterCreated Jun 23, 2026Updated Sep 2, 2026
Labelskind/featurehelp wanted

Summary

When a TaskRun has multiple steps that each emit results, every step container's termination message contains all previous steps' results plus its own — they accumulate as steps complete. This contradicts the Tasks docs:

If your Task writes a large number of small results, you can work around this limitation by writing each result from a separate Step so that each Step has its own termination message.

Splitting results across steps actually makes the 4KB MaxContainerTerminationMessageLength limit easier to hit: the last step's container must hold the full accumulated payload. The advice was likely true ~6 years ago but is now stale.

Reported by @aThorp96, confirmed empirically: a generate-results-at-once Task succeeds while an equivalent generate-results-sequentially Task picks up only the first few results and then fails because the termination message is truncated at 4KB and no longer valid JSON.

Root cause

Two facts combine:

  1. All step containers share one termination file. In pkg/pod/entrypoint.go, terminationPath = "/tekton/termination" is passed to every step (-termination_path arg + steps[i].TerminationMessagePath). /tekton is a shared volume, so all containers point at the same file.
  2. The entrypointer appends. pkg/termination/write.go does pro = append(existing, pro...). This append is required intra-container (results + exit code + reason are separate writes), but the shared file makes it accumulate across containers too.

The controller does not rely on the accumulation: setTaskRunStatusBasedOnStepStatus (pkg/pod/status.go) parses each container's message independently, filters step results per-step, and dedupes task results via removeDuplicateResults — dedup exists precisely to clean up this redundancy.

It's a 2019 artifact: only task results existed then, and the shared file gave "merge for free". Per-step filtering + dedup were added later instead of fixing the root coupling.

Proposed fix: per-step termination files

Give each step its own file, e.g. /tekton/termination-{stepIndex}:

  • pkg/pod/entrypoint.go: per-step TerminationMessagePath and matching -termination_path arg.
  • cmd/entrypoint/main.go already takes -termination_path per step; controller already reads each container's message independently — no read-side change.

This restores an independent 4KB budget per step and makes the documented workaround actually true.

Caveats to validate

  1. Status/pod unit tests hardcode /tekton/termination and the accumulated shape — need updating.
  2. Confirm nothing in breakpoint/debug or results-sidecar paths reads the fixed path (grep suggests only entrypoint.go + main.go).
  3. Entrypoint image and controller ship pinned together, so the path change is internal.
  4. removeDuplicateResults and intra-container append remain as a harmless safety net.

Docs follow-up

Correct/remove the stale "write each result from a separate Step" workaround in docs/tasks.md; point to the compressed termination message option (tknz: prefix) instead.

cc @aThorp96