Buildx failed with: failed to receive status
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.
#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: EOFThe 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 |
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
- Look at the version of
docker/build-push-actionthe failing job used. - On v7.3.0 or earlier the doubled ERROR prefix is normal and tells you nothing.
- 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.
- uses: docker/build-push-action@v7
# pin to a release, not a floating major, if you compare logs across monthsGive 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.
- 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 dfReduce peak pressure instead of retrying into the same wall
- Cap builder parallelism so fewer compilers run at once.
- Split a heavy stage so its peak is spread over two steps.
- Move to a larger runner if the peak is genuinely what the build needs.
- uses: docker/setup-buildx-action@v4
with:
driver-opts: |
env.BUILDKIT_MAX_PARALLELISM=2Retry 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.
- 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 .
}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.
# 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=10485760Why 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.
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.
Frequently asked questions
Why does my log say ERROR twice?
Is failed to receive status the same as a canceled build?
Should I retry a build that fails this way?
Does the gRPC code tell me anything useful?
Related guides
References
- build-push-action: where the buildx failed with prefix is built
- actions-toolkit: the getErrorMessage helper added in v7.4.0
- BuildKit: the status receive loop that returns this error
- docker/buildx#3967: gRPC keepalive for the Kubernetes driver
- earthly/earthly#294: the same client error outside Docker tooling
- Docker documentation
- Docker build cache
- GitHub Actions documentation