Running Concurrent Docker Builds Against One Cache

Several GitHub Actions jobs can build on one WarpBuild builder profile at once. What the shared layer cache does under load, how to size it, what it bills.

Jobs that name the same builder profile build in parallel on one builder VM and read one layer cache, and that cache is shared but eventually consistent, so two builds running at the same instant can each build the same base stage. The documented sizing recommendation is roughly 8 vCPU and 16 GB of memory per concurrent build job, so the width of your fan-out decides whether adding another image makes every image slower.

This guide covers the documented behavior of one profile under load, the two workflow shapes that relieve contention, the knobs that control it, and a cost model in builder minutes for adding a second profile. Rates come from the pricing page and the remote Docker builders documentation, checked on 2026-08-13. For the wider picture, start at Docker builds on GitHub Actions.

Diagnosis

The signature of profile contention is a build step whose duration tracks the width of the matrix. Three images take four minutes each, six images take eight, and the Dockerfile never changed.

Four documented behaviors explain what a profile does when several builds arrive together:

  • One builder profile is one dedicated Docker builder VM with caching, and multiple jobs using the same profile run their builds on that VM in parallel.
  • There is no limit on how many builds run concurrently on a profile. The recommended minimum resource requirement is approximately 8 vCPU and 16 GB of memory per build job.
  • The cache is shared and eventually consistent. Layers produced by one in-flight build may not be visible to another build running at the same moment, and they become available to later builds after synchronization occurs.
  • Builders bill per session. Jobs with overlapping execution on one profile share a single session, measured from the first job's builder start to the last job's completion. A multi-architecture profile opens one session per architecture, each billed independently.

The first two produce CPU contention: six builds on a 16 vCPU profile hold about 2.7 vCPU each against a documented recommendation of 8. The third produces duplicated work: a cold fan-out builds the shared base stage in more than one leg, and the run after it reads a single copy.

Confirm which one you have before changing anything. Trigger a single matrix leg on its own with workflow_dispatch and compare its build step duration against the same leg inside the full matrix. If the solo leg finishes sooner, the fan-out is contending for the profile. If the solo leg is equally slow, the problem is layer invalidation rather than concurrency, and sharing a Docker layer cache across jobs covers that path.

Then read the logs of a cold run. Count CACHED lines on the second and later legs. Legs that all execute the same base stage with no CACHED prefix are hitting eventual consistency, which costs CPU on the profile and makes the contention worse.

Runner capacity is a separate question from builder capacity. Run as many jobs as your workflows need. Generally available Linux and Windows runners do not have plan-level concurrency caps, so the queue you see is on the profile rather than on the runner pool.

Fix

There are two shapes, and they trade different resources.

The first shape keeps one builder profile and sizes it for the width of the fan-out. Six concurrent builds at the documented 8 vCPU recommendation ask for 48 vCPU, so a 64 vCPU profile carries them:

name: images
on:
  pull_request:

jobs:
  build:
    runs-on: warp-ubuntu-latest-x64-2x
    strategy:
      fail-fast: false
      matrix:
        service: [api, worker, scheduler, gateway, indexer, web]
    steps:
      - uses: actions/checkout@v4

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - uses: Warpbuilds/build-push-action@v6
        with:
          context: .
          file: services/${{ matrix.service }}/Dockerfile
          push: true
          tags: ghcr.io/acme/${{ matrix.service }}:${{ github.sha }}
          profile-name: services-amd64
          timeout: 900000

The job runner stays small because the build runs on the builder, so each leg checks out the repository, authenticates to the registry, and waits. Pick a Linux runner for the driving job, since the build itself executes on the builder. Remote Docker builders sit alongside snapshot runners, CI observability, an MCP server, and the Action Debugger in the product surface.

The second shape splits the fan-out across two profiles, which halves the builds per VM without buying a larger size. Carry the profile name in the matrix:

    strategy:
      fail-fast: false
      matrix:
        include:
          - service: api
            profile: services-core
          - service: worker
            profile: services-core
          - service: scheduler
            profile: services-core
          - service: gateway
            profile: services-edge
          - service: indexer
            profile: services-edge
          - service: web
            profile: services-edge
    steps:
      - uses: actions/checkout@v4

      - uses: Warpbuilds/build-push-action@v6
        with:
          context: .
          file: services/${{ matrix.service }}/Dockerfile
          push: true
          tags: ghcr.io/acme/${{ matrix.service }}:${{ github.sha }}
          profile-name: ${{ matrix.profile }}

Two profiles mean two caches. Shared base layers are stored once per profile, so a cold run after a base image bump builds that stage on both. Group the services so each profile holds one working set: images that share a base and a dependency stage belong together, and images with nothing in common are the natural split line.

Configuration

Profile size is the main knob, and it divides by concurrency rather than by image count. This table is arithmetic on the documented 8 vCPU recommendation, using the sizes and rates from the pricing page:

Profile sizeDiskPrice per minute3 concurrent builds6 concurrent builds12 concurrent builds
16 vCPU, 32 GB100GB$0.065.3 vCPU2.7 vCPU1.3 vCPU
32 vCPU, 64 GB200GB$0.1210.7 vCPU5.3 vCPU2.7 vCPU
64 vCPU, 128 GB200GB$0.2421.3 vCPU10.7 vCPU5.3 vCPU
96 vCPU, 192 GB600GB$0.3632.0 vCPU16.0 vCPU8.0 vCPU
192 vCPU, 384 GB600GB$0.7264.0 vCPU32.0 vCPU16.0 vCPU

Cells at 8 vCPU or above meet the documented minimum. Memory divides the same way, and it is often the tighter constraint: 32 GB across six builds leaves 5.3 GB each against a recommendation of 16 GB. The 96 vCPU and 192 vCPU sizes also come with a 2TB disk at $0.52 and $0.88 per minute, and both are amd64 only. arm64 and multi-architecture profiles cap at 64 vCPU, so a 12-wide arm64 fan-out cannot be solved by size alone and needs the split shape above.

Four other settings change what contention looks like:

  • max-parallel on the matrix strategy caps how many legs run at once. Six legs at max-parallel: 3 give each build twice the vCPU of a full-width fan-out, and they stretch the session, since the session runs until the last leg completes.
  • timeout on the WarpBuild action is the wait for a builder to be ready, in milliseconds, and it defaults to 10 minutes. Raise it when a burst of jobs arrives at a profile that is still starting.
  • platforms with two architectures runs each architecture on a separate builder instance, which means one session per architecture and two sets of layers to keep warm.
  • Warpbuilds/docker-configure@v1 is the entry point when you run your own build command. Invoke it immediately before the build step, because the builder is billed from the moment it is assigned.

The builder cache has a TTL of 10 days and a profile that goes unused for longer is reset automatically. Dependency caches that live outside the Docker build follow separate rules and are covered in the WarpBuild caching documentation.

Cost or Time Model

Everything below is arithmetic on stated assumptions rather than a measurement. Replace each duration with the numbers in your own job logs before deciding on it.

Assumptions:

  • Six service images from one shared base stage, 200 pull request runs per month.
  • A build that gets the recommended 8 vCPU occupies the builder for 4.0 minutes: 3.0 minutes of CPU-bound work and 1.0 minute of context transfer and registry push.
  • CPU-bound time scales with the vCPU shortfall. At 2.7 vCPU the CPU-bound part runs 9.0 minutes instead of 3.0; at 5.3 vCPU it runs 4.5 minutes.
  • Matrix legs get runners within 0.5 minutes of each other, so a session runs 0.5 minutes longer than one build.
  • 0.5 minutes of job runner overhead per leg for checkout and registry login, on warp-ubuntu-latest-x64-2x at $0.004 per minute.
OptionBuilder minutes per runBuilder costRunner minutesRunner costCost per run200 runsFan-out wall clock
One 16 vCPU profile10.5$0.6363$0.25$0.88$176.4010.5 min
Two 16 vCPU profiles12.0$0.7236$0.14$0.86$172.806.0 min
One 64 vCPU profile4.5$1.0827$0.11$1.19$237.604.5 min

Three readings come out of that.

Contention is billed. Session billing measures wall clock, so the minutes a build spends waiting on vCPU land on the builder line. The single 16 vCPU profile bills 10.5 builder minutes for the same six images that occupy 4.5 builder minutes on the 64 vCPU profile, and the lower rate is what keeps its total down.

The split buys wall clock for roughly nothing. Two profiles open two sessions and add 1.5 builder minutes per run, and the shorter jobs hand back 27 runner minutes, so the monthly totals sit within $4 of each other while the fan-out finishes 4.5 minutes sooner. The cost is cache density: after a base image bump, each profile builds the shared stage once, so the first run of the day is slower on both.

The larger profile is the fastest option and the most expensive one. It keeps a single cache for all six images and finishes in 4.5 minutes, for $64.80 more per month than the split on these assumptions.

Every rate here carries its source and a checked-on date, and the same rates appear on the pricing page and in the remote Docker builders documentation.

FAQ

What happens when six builds hit one builder profile at the same time?

They run in parallel on the same builder VM against the same layer cache. There is no documented limit on concurrent builds per profile, and the documented recommended minimum is roughly 8 vCPU and 16 GB of memory per concurrent build job, so a 16 vCPU profile carrying six builds gives each one about 2.7 vCPU.

Why do matrix builds get slower as the matrix gets wider?

Two effects stack. The legs divide one builder's vCPU between them, and the profile cache is eventually consistent, so legs that start together can each build the same base stage instead of reading it from the cache. Re-run one leg on its own and compare the build step duration to confirm which one you are looking at.

Does splitting a fan-out across two profiles cost more?

In builder minutes, yes, because each profile opens its own session. The shorter jobs give runner minutes back, so on the six-image model in this guide two 16 vCPU profiles land within $4 per month of one, while fan-out wall clock drops from 10.5 minutes to 6.0 minutes.

Start with $10 in free credits

Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.