# GitHub Actions GITHUB_STEP_SUMMARY too large, and the cap that decides

> GitHub Actions GITHUB_STEP_SUMMARY too large is a fixed 1024k cap in the runner. The message reports both numbers, and the step still passes.

Source: https://latchkey.dev/learn/github-actions/gha-step-summary-too-large  
Updated: 2026-09-21

A GitHub Actions GITHUB_STEP_SUMMARY too large failure is the runner refusing to attach a summary file that is over a fixed one-mebibyte cap, and saying so with both numbers in kibibytes. The refusal is an annotation rather than a step result, so the usual shape of this problem is a red annotation on a green job and a run page with no summary on it.

## What this error means

A summary you wrote is missing from the run page, and somewhere in the step there is a line beginning `$GITHUB_STEP_SUMMARY upload aborted`. The message carries two numbers: the cap and what your file actually was, both in kibibytes. The cap is `1024k` and does not vary by plan, runner or repository, because it is a constant compiled into the runner. There is a quieter version of the same problem with no message at all, when the file is empty, and a third shape where the summary from an earlier step is simply not there, which is not a size problem and is not reported anywhere.

```Actions log, quoted from patrick-werner/fhir-html-validation-to-markdown#2
Error: $GITHUB_STEP_SUMMARY upload aborted, supports content up to a size of 1024k, got 1254k. For more information see: https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-markdown-summary
```

## Common causes

### The summary file is genuinely over 1024k

The direct cause and the one the message states. It is normally a report, a log or a generated table appended wholesale, and it often crosses the cap on a bad day rather than every day, which is why it shows up as an intermittent missing summary rather than a consistent one. The second number in the message tells you by how much.

### A tool inside the job writes the summary and does not bound it

Linters, test reporters and coverage tools frequently append to `$GITHUB_STEP_SUMMARY` on your behalf, so the size is decided by how many findings there were rather than by anything in your workflow. In our experience this is the version people find hardest to place, because nothing in the workflow file mentions the summary at all.

### The summary is empty and was skipped without a word

A zero-byte file is skipped before the size check, with only an internal trace. If the run page shows no summary and the log shows no message either, this is the branch you are in, and the question is why the writing step produced nothing.

### The append happened in a different step, or after the step ended

Each step is given a fresh file and the previous one is deleted, and the attach runs immediately after the step's handler returns. Cross-step accumulation and writes from a backgrounded process both land in a file that is never read. No message is produced, so this reads as a missing summary rather than as an error.

## How to fix it

### Read both numbers before changing anything

1. Find the `upload aborted` line and take the two sizes out of it.
2. The first is always 1024k; the second is your file, so the ratio tells you whether this is a trim or a rethink.
3. If there is no such line and the summary is still missing, the file was empty or written outside the step, and the size is not the problem.

### Move the bulk to an artifact and link it

Upload the full report as an artifact and keep the summary to the headline numbers and a link. This is the fix that survives the report growing, because the summary size stops depending on how many findings there were.

```.github/workflows/ci.yml (illustrative)
- uses: actions/upload-artifact@v4
        with:
          name: build-report
          path: report.md
      - run: |
          echo "## Build report" >> "$GITHUB_STEP_SUMMARY"
          echo "Full report: see the build-report artifact." >> "$GITHUB_STEP_SUMMARY"
```

### Bound the writer rather than the file

If a tool is producing the content, configure its own limit where it has one, or pipe it through something that caps the output before it reaches the file. Truncating after the fact works, but capping at the source keeps the summary readable instead of cutting it mid-table.

### Write the summary in the step that produced the content

1. Move every append into the step whose output it describes.
2. If several steps need to contribute, have each write its own summary rather than trying to accumulate one.
3. Never append from a process that outlives its step; the attachment happens as soon as the step's handler returns.

## How to prevent it

- Treat the summary as a headline, and give anything longer to an artifact.
- Cap generated tables at a fixed number of rows and say how many were omitted.
- Check for the `upload aborted` annotation in runs that are green, because nothing else will point at it.
- Keep each step's summary self-contained, since the runner hands every step a fresh file.

## One constant, three outcomes

`CreateStepSummaryCommand` in the runner's `FileCommandManager.cs` declares `AttachmentSizeLimit` as `1024 * 1024`, stats the file, and then takes one of three paths. The message you are reading is formatted from `UnsupportedSummarySize` in `Constants.cs`, with the limit and the file size both divided by 1024 before they are substituted, which is why both numbers are reported with a `k`.

The other two paths are silent. An empty file is skipped with nothing written to the log at all, and a file within the cap is masked for secrets, copied and queued for upload. The table is those three, in the order the code tests them.

| File size | What the runner does | What the log shows |
| --- | --- | --- |
| Zero bytes | Skips the attachment entirely | Nothing |
| Up to 1024k | Masks secrets line by line, then queues the upload | Nothing |
| Over 1024k | Writes an error annotation and returns | The `upload aborted` line with both sizes |

> The masking pass is worth knowing about for a different reason: the runner reads your summary line by line and rewrites it through the secret masker before anything is uploaded, so a value that matches a registered secret appears redacted in the published summary.

## Why the job can still be green

The size branch calls `context.Error(...)` and returns. It does not set `CommandResult`, and `CommandResult` is the value `StepsRunner` merges into the step result when the step finishes. `AddIssue` on the execution context increments an error count, writes the line with the error tag, and queues a timeline update; it does not change the result.

So the annotation is real and visible on the run page while the step and the job carry on. That combination is what makes this one hard to notice in a busy workflow: the run is green, nothing is retried, and the only durable trace is an annotation and a missing summary.

## Every step gets its own file, and the previous one is deleted

`InitializeFiles` runs before each step. It generates a fresh identifier, deletes the file the previous step was using, creates an empty file under the new name, and points the `step_summary` context, which is what `$GITHUB_STEP_SUMMARY` resolves to, at that new path. `ProcessFiles` then runs in a `finally` after the step's handler has completed, reads whatever is there and attaches it.

Two consequences follow, and both get mistaken for size problems. Appending to `$GITHUB_STEP_SUMMARY` from step A and expecting step B to add to the same document does not work, because B is writing to a different file and A's was deleted. And a background process that writes after its step has finished writes to a path nobody will read, because the attach already happened.

```.github/workflows/ci.yml (illustrative)
- name: Write the summary in the step that produced it
        run: |
          {
            echo "## Test results"
            echo ""
            echo "| Suite | Result |"
            echo "| --- | --- |"
            echo "| unit | pass |"
          } >> "$GITHUB_STEP_SUMMARY"
```

## Getting under the cap without losing the detail

The cap is on bytes, not on rendered length, so the cheapest wins are usually structural. A summary that embeds a full lint report, a diff or a test log is carrying content that reads better as a downloadable artifact anyway, and a link to that artifact costs a few dozen bytes.

Where the content genuinely belongs in the summary, truncate deliberately rather than letting the runner drop the whole thing. A summary cut to the first few hundred failures with a line saying how many were omitted is strictly better than no summary at all, which is what the cap gives you otherwise.

```.github/workflows/ci.yml (illustrative)
- name: Keep the summary inside the cap
        run: |
          head -c 900000 report.md >> "$GITHUB_STEP_SUMMARY"
          echo "" >> "$GITHUB_STEP_SUMMARY"
          echo "_Truncated. Full report is in the build-report artifact._" >> "$GITHUB_STEP_SUMMARY"
```

## Why there is no recorded run on this page

The decision this page is about is a file size compared against a compiled-in constant, and a run of ours would add one more pair of numbers to a message that already prints both of the ones that matter. The interesting parts, that the check is a hard constant rather than a quota, that the branch writes an annotation without touching the step result, and that each step is handed a new file, are all statements about the runner's source and are more precisely settled by reading it than by producing a log that restates the cap.

## FAQ

### What is the actual size limit for a GitHub Actions job summary?

One mebibyte per step. The runner declares `AttachmentSizeLimit` as `1024 * 1024` in `CreateStepSummaryCommand` and reports it as `1024k` because it divides both numbers by 1024 before formatting the message. It is a constant in the runner rather than a per-plan quota.

### Why did my job pass when the summary upload was aborted?

Because the size branch writes an error annotation and returns without setting `CommandResult`, and `CommandResult` is what the step runner merges into the step result. The annotation is recorded and shown, and the step keeps whatever result its own process produced.

### Can I append to the same summary from several steps?

No. The runner creates a new summary file before each step, deletes the previous step's file, and repoints `$GITHUB_STEP_SUMMARY` at the new path, then attaches the file as soon as the step finishes. Each step writes its own summary or none.

### My summary is missing and there is no message at all. What happened?

Most likely the file was empty, which the runner skips before it reaches the size check and reports only in an internal trace. The other silent case is a write that happened after the step ended, since the attachment runs in a `finally` immediately after the step's handler returns.

## References

- [actions/runner: FileCommandManager.cs, CreateStepSummaryCommand and the per-step file lifecycle](https://github.com/actions/runner/blob/main/src/Runner.Worker/FileCommandManager.cs)
- [actions/runner: Constants.cs, the UnsupportedSummarySize format string](https://github.com/actions/runner/blob/main/src/Runner.Common/Constants.cs)
- [GitHub Docs: workflow commands, adding a job summary](https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions)
- [patrick-werner/fhir-html-validation-to-markdown#2: the aborted upload quoted here](https://github.com/patrick-werner/fhir-html-validation-to-markdown/issues/2)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
