Skip to content
Latchkey LogoLatchkey home

Docker failed to solve: rpc error: code = Canceled in CI

Docker failed to solve: rpc error: code = Canceled is not a fault report. It is BuildKit relaying that the call carrying your build was canceled, in wording that comes from gRPC and from the Go standard library rather than from Docker, and the only useful question is which program did the canceling.

Diagram of four programs contributing to a canceled build line and the callers that cancel
The words "rpc error: code = Canceled desc =" are formatted by gRPC and "context canceled" by the Go standard library. Only "failed to solve" belongs to Docker tooling.

What this error means

A build that was making progress stops partway. One step shows as CANCELED rather than as an error, and the job ends with a solve failure carrying a gRPC code and the words context canceled. No step reported a problem of its own, and rerunning the same commit frequently succeeds. The block below is assembled from the strings each program formats, not captured from a job.

Reconstructed from the formatting in buildx, the BuildKit client, gRPC and Go context
#14 [build 5/7] RUN go build ./...
#14 CANCELED
ERROR: failed to solve: rpc error: code = Canceled desc = context canceled

Nothing in this line is a Docker diagnosis

Take the sentence apart and very little of it is Docker. The BuildKit Go client contributes "failed to solve", and that is all. The middle is produced verbatim by the gRPC Go library, whose status type formats itself as the words rpc error, a code and a description. The description here is the text of Go's own cancellation error, which is the two words context canceled and nothing else.

That is why searching the whole line is unproductive: it exists as a literal in no source file and every program that uses gRPC in Go can produce the middle of it. The Docker specific part of your problem is entirely in the question of who canceled the context, and the log line cannot answer that because by the time it is printed the cancellation has already propagated.

FragmentLibrary that formats itWhat it tells you
failed to solve:The BuildKit Go clientThe failure happened in the build call, not in setup
rpc error: code = Canceled desc =gRPC for Go, its status typeThe call ended in cancellation rather than an error
context canceledThe Go standard library context packageSomething called cancel, nothing timed out or crashed
#14 CANCELEDBuildKit progress printerThat step ended in cancellation, decided by a string test

Common causes

The job hit its timeout

The simplest cause and the easiest to confirm, because the job duration will sit right on the configured limit. Note that a job with no timeout-minutes still has a platform limit, so an unset timeout is not the same as no timeout. A build that grew past its budget produces this line every time rather than intermittently.

A concurrency group canceled the run

With cancel in progress set, a new push cancels the previous run for the same group, and every job in it stops wherever it was. This presents as intermittent failures that correlate with how often people push, which is a pattern worth recognizing because it looks like flakiness and is not. The run page says the run was canceled.

The builder or the runner went away underneath the build

A builder container killed for memory, a runner reclaimed, or a self hosted machine restarting all take down the far end. Whether you get a cancellation or a status failure depends on the order in which things tear down, which is why this cause appears on two pages. If the run page shows failure rather than cancellation, this is the branch you are on.

buildx canceled its own solve after the stream went quiet

The client cancels a solve that is already failing once its status stream has been inactive for a few seconds, so the command exits rather than hanging. That converts a different underlying problem into a Canceled line. Treat a cancellation with no timeout and no cancel in progress rule as a pointer to look for what was going wrong before it.

How to fix it

Look at the run page before you look at the build

  1. If the run is marked canceled, the cause is outside the build and the build log has nothing more to give you.
  2. Compare the job duration to timeout-minutes. Equal means the timeout, not the build.
  3. If the run is marked failed and nothing canceled it, treat the line as a symptom and look at the last step that made progress.

Scope the concurrency group so unrelated work does not cancel your builds

A group keyed only on the workflow cancels across branches and across unrelated pull requests. Keying it on the ref as well means a push cancels only its own previous run, which is almost always what people meant.

.github/workflows/build.yml
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Make the build finish comfortably inside its budget

Raising the timeout is the fix only when the build is genuinely that long. Otherwise the cheaper move is to stop redoing work: a registry or GitHub Actions cache on the build turns a cold build into a warm one and usually moves the duration far enough from the limit that the cause stops mattering.

.github/workflows/build.yml
- uses: docker/build-push-action@v7
  with:
    context: .
    cache-from: type=gha
    cache-to: type=gha,mode=max

Keep the step that tells you where it stopped

Plain progress prints each step as it completes, including the CANCELED marker, so a failed job leaves a readable record of how far it had got. The fancy renderer collapses that. In CI the trade is worth it: the log is longer and it answers the only question the cancellation line cannot.

.github/workflows/build.yml
env:
  BUILDKIT_PROGRESS: plain

CANCELED in the step list is decided by matching text

The plain progress printer chooses between printing a step as CANCELED and printing it as an error by testing whether the step's error text ends with the string context canceled. It is not reading a status code. That is worth knowing because it means the pretty CANCELED marker and the gRPC code at the bottom are two independent judgements about the same thing, and an error that merely happens to end in those words is displayed the same way.

In practice the two agree. The reason to know they are separate is that the step level marker tells you which step was in flight when the cancel arrived, and that is often the most useful fact in the whole log, because it tells you whether the build was near its end or nowhere near it.

Four callers, and the workflow usually knows which one

GitHub cancels a job for a timeout, for a concurrency group with cancel in progress, or because a person pressed the button. All three send the step process a signal, buildx cancels its context, and this is what comes out. The run page tells you which, and the job duration against timeout-minutes settles the timeout case on its own.

The fourth caller is buildx itself. The client watches the status stream for activity, and when a solve is already failing and the stream goes quiet it cancels the solve after a short wait so the command does not hang forever. That path turns some other underlying failure into a cancellation, which is why a Canceled line with a green run page and no timeout is worth reading as a symptom of something else rather than as the problem.

.github/workflows/build.yml
# the concurrency rule that cancels the previous run on a new push
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  image:
    timeout-minutes: 45

Why no recorded run backs this page

A recorded run of a cancellation is a recording of us canceling something. We would set a one minute timeout on a five minute build, capture the line, and present a self inflicted stop as evidence about your pipeline. The mechanism is not in the runner at all: it is in which process calls cancel and how that travels through gRPC.

So the work went into the attribution instead. Each fragment of the line is traced to the library that formats it, including the two that are not Docker, and the CANCELED marker is traced to the string test that produces it. A reader who understands that the middle of the line is generic gRPC output stops trying to find a Docker explanation for it, which is the whole value of the page.

How to prevent it

  • Set an explicit timeout-minutes on build jobs so a cancellation has a number you can compare against.
  • Key concurrency groups on the ref as well as the workflow.
  • Cache layers so a cold build is not routinely close to the time limit.
  • Run builds with plain progress in CI so the step that was in flight is always in the log.

Frequently asked questions

Is rpc error: code = Canceled a Docker error?
Only the first three words of the line are Docker tooling. The rest is the gRPC library formatting a canceled call and the Go standard library naming its own cancellation. Any Go program that uses gRPC produces the same middle, which is why searching the whole line finds unrelated projects.
Why does one step say CANCELED instead of ERROR?
The progress printer tests whether that step's error text ends with the words context canceled and prints CANCELED when it does. It is a string test rather than a status code check. The practical value of the marker is that it names the step that was running when the cancellation arrived.
Does a canceled build mean my Dockerfile is wrong?
No. A cancellation means something stopped the build from outside: a timeout, a concurrency rule, a person, or the builder going away. Nothing in the Dockerfile can produce this line. Check the run page first, because it usually says which of those it was.
Why do I get this when nothing canceled my job?
The BuildKit client cancels its own solve when a build is already failing and its status stream has gone quiet, so the command exits rather than hanging. In that case the cancellation is downstream of a different problem, and the step that last made progress is where to look.

Related guides

References

A canceled build still bills. Latchkey makes it shorter with a layer cache that persists between jobs. Start free → 30-day trial · No credit card