# Buildx failed with: failed to receive status

> Buildx failed with: failed to receive status is written by the GitHub Action, not by buildx, and the action changed how it builds that line in v7.4.0.

Source: https://latchkey.dev/learn/docker/docker-buildx-failed-to-receive-status  
Updated: 2026-09-21

Buildx failed with: failed to receive status is two sentences from two programs: the prefix is written by the GitHub Action wrapping buildx, and the rest is the BuildKit Go client reporting that the stream carrying build progress broke. The action rebuilt that prefix in v7.4.0, so logs from before and after that release do not look the same for the same failure.

## What this error means

A build runs normally for a while, the progress output stops mid step, and the job ends with a line naming a status failure and a gRPC code. Rerunning often succeeds. The tell is that the message is about the status stream rather than about the build: BuildKit was still working when the client stopped being able to hear it. The block below is assembled from the strings each program formats, not captured from a job.

```Reconstructed from the formatting in build-push-action, buildx and the BuildKit client
#14 [build 5/7] RUN go build ./...
ERROR: buildx failed with: ERROR: failed to build: failed to receive status: rpc error: code = Unavailable desc = error reading from server: EOF
--- on docker/build-push-action v7.4.0 and later the same failure reads
ERROR: buildx failed with: failed to build: failed to receive status: rpc error: code = Unavailable desc = error reading from server: EOF
```

## Common causes

### The builder container was killed for memory

A `docker-container` builder is a container with the same memory limits as anything else on the runner. A build that compiles in parallel can take it past them, the kernel kills it, and the client sees its stream end. The build output gives no hint, because the process that would have reported the problem is the one that died. In our experience this is the first thing to rule out on a self hosted or small runner.

### The runner filled its disk under the builder

The builder writes layers, cache mounts and the context into the same filesystem as everything else in the job. When that fills, buildkitd fails in ways it cannot report cleanly and the stream is what you notice. A disk check in the job before and after the build separates this from memory in one run.

### An idle connection to a remote builder was dropped

With a remote or Kubernetes driver the status stream crosses real network equipment with its own idle timeouts. buildx added gRPC keepalive to the Kubernetes driver for exactly this, so the buildx version behind your builder matters. A long step that produces no output is the classic trigger, because the stream has nothing to carry while the step runs.

### The job itself was ended by the platform

A runner reclaimed mid job, or a job canceled by a concurrency rule, takes the client and the builder down together. This case can produce the status message when the teardown order happens to break the stream before the cancellation reaches the client. If your workflow cancels in progress runs on new pushes, check whether the failing runs were superseded.

## How to fix it

### Date your log before you debug it

1. Look at the version of `docker/build-push-action` the failing job used.
2. On v7.3.0 or earlier the doubled ERROR prefix is normal and tells you nothing.
3. On v7.4.0 or later a single prefix is normal. A log with two prefixes from a recent run means the second one is part of the real message.

```.github/workflows/build.yml
- uses: docker/build-push-action@v7
  # pin to a release, not a floating major, if you compare logs across months
```

### Give the builder room and prove which resource ran out

Measure rather than guess: print memory and disk immediately after the failing build so the next occurrence tells you which one it was. Both produce the same status message, and the remedies are different.

```.github/workflows/build.yml
- name: Build
  id: build
  continue-on-error: true
  uses: docker/build-push-action@v7
  with: { context: . }

- name: What ran out
  if: steps.build.outcome == 'failure'
  run: |
    df -h /
    free -m || vm_stat
    docker system df
```

### Reduce peak pressure instead of retrying into the same wall

1. Cap builder parallelism so fewer compilers run at once.
2. Split a heavy stage so its peak is spread over two steps.
3. Move to a larger runner if the peak is genuinely what the build needs.

```.github/workflows/build.yml
- uses: docker/setup-buildx-action@v4
  with:
    driver-opts: |
      env.BUILDKIT_MAX_PARALLELISM=2
```

### Retry only the case that is actually transient

A dropped connection to a remote builder is worth one retry. A builder that is killed for memory every time is not, and a retry there costs you the whole build again for the same outcome. Bound it, and log which branch you took so the pattern is visible after a week.

```.github/workflows/build.yml
- name: Build with one retry
  run: |
    docker buildx build -t acme/api:ci . || {
      echo "first attempt failed, retrying once"
      docker buildx build -t acme/api:ci .
    }
```

## How to prevent it

- Pin the build action to a release rather than a floating major, so the message format does not change under you.
- Keep a named builder across jobs instead of creating a fresh container for every build.
- Print disk and memory after a failed build so the next occurrence names its own cause.
- Upgrade buildx when you use a remote or Kubernetes driver, where keepalive fixes land.

## The prefix is the Action, and it changed on 15 September 2026

buildx never prints the words "buildx failed with". They come from `docker/build-push-action`, which runs the buildx command, and on a non zero exit builds an error out of that literal prefix and a slice of the command's standard error.

How it takes that slice changed. From v3.0.0 through v7.3.0 the action matched the last line of stderr and pasted it verbatim, which is why so many logs read "buildx failed with: ERROR: failed to build: ..." with the word ERROR twice: buildx had printed its own prefix and the action kept it. Version 7.4.0, released on 15 September 2026, replaced that with a helper that scans stderr backwards for the last line beginning with ERROR: and strips that prefix before pasting. Same failure, different line.

| Action version | How it slices buildx stderr | Whether ERROR appears twice |
| --- | --- | --- |
| v3.0.0 through v7.3.0 | The last line of stderr, pasted whole | Yes |
| v7.4.0 and later | The last line starting with ERROR:, with that prefix removed | No |
| Any version, `call: check` | The first line of stdout instead | Not applicable |
| Any version, no ERROR: line at all | The last non empty line of stderr | No |

> Read in docker/build-push-action src/main.ts across tags v3.0.0 to v7.4.0 and in docker/actions-toolkit src/buildx/buildx.ts on 2026-09-21. v7.4.0 published 2026-09-15.

## What "failed to receive status" actually reports

Inside the BuildKit Go client, a build runs two things at once: the solve call that does the work, and a status stream that carries progress back to you. The sentence comes from the receive loop on that stream, and it is deliberately narrow. Before returning it, the loop checks whether the error is a cancellation, and returns success if it is. So a canceled build never produces this message; a canceled build produces the Canceled wording covered on the page linked below.

That leaves one meaning: the stream to buildkitd broke for a reason that is not cancellation. The gRPC code carries the detail, and Unavailable with "error reading from server: EOF" is the shape you get when the process at the other end went away rather than answering.

## Where the other end goes when it goes

On a `docker-container` builder the other end is a container on the same runner, so it disappears when it is killed for memory, when the disk under it fills, or when the runner itself is reclaimed. On a remote or Kubernetes driver it is across a network, and the failure mode is a connection that was idle long enough for something in between to drop it.

That second case has a concrete fix upstream rather than a workaround: buildx added gRPC keepalive to the Kubernetes driver specifically because idle connections were being dropped and builds were hanging or failing on the status stream. If your builder is remote, the version of buildx you run is part of the diagnosis.

```.github/workflows/build.yml
# make the builder a named, reused one rather than a fresh container per job
- uses: docker/setup-buildx-action@v4
  with:
    driver: docker-container
    name: ci-builder
    driver-opts: |
      env.BUILDKIT_STEP_LOG_MAX_SIZE=10485760
```

## Why no recorded run backs this page

To record this honestly we would have to kill the builder in the middle of a build and publish the result as though it were a thing that happened to us. It would be a picture of us pulling a plug. The failure is real and common, but the mechanism that makes it interesting is a client library returning one specific error rather than another, and a runner adds nothing to that.

The claim that is worth being careful about on this page is the version boundary in the Action, and that one is checkable exactly: we read the same line of source at seven tags and can name the release that changed it. That is stronger evidence than a screenshot of one job, and it is the part a reader can use to date their own log.

## FAQ

### Why does my log say ERROR twice?

Because the GitHub Action pasted the last line of buildx stderr verbatim, and buildx had already printed its own ERROR prefix. That was how the action worked up to v7.3.0. From v7.4.0 it strips the prefix before pasting, so a recent log has one.

### Is failed to receive status the same as a canceled build?

No, and the client makes a point of distinguishing them. The receive loop checks for cancellation before it returns this error and reports success instead when that is what happened. A canceled build gives you a Canceled gRPC code in a different sentence.

### Should I retry a build that fails this way?

Once, if your builder is remote, because a dropped connection is genuinely transient. If the builder is a container on the same runner and it is being killed for memory, a retry buys you the same failure and the same minutes. Measure which one you have before you build a retry into the workflow.

### Does the gRPC code tell me anything useful?

It tells you how the stream ended. Unavailable with a message about reading from the server means the far end stopped answering, which points at the builder rather than at your build. The code is produced by the gRPC library, not by Docker, so it carries no Docker specific meaning.

## References

- [build-push-action: where the buildx failed with prefix is built](https://github.com/docker/build-push-action/blob/master/src/main.ts)
- [actions-toolkit: the getErrorMessage helper added in v7.4.0](https://github.com/docker/actions-toolkit/blob/main/src/buildx/buildx.ts)
- [BuildKit: the status receive loop that returns this error](https://github.com/moby/buildkit/blob/master/client/solve.go)
- [docker/buildx#3967: gRPC keepalive for the Kubernetes driver](https://github.com/docker/buildx/pull/3967)
- [earthly/earthly#294: the same client error outside Docker tooling](https://github.com/earthly/earthly/issues/294)

---

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
