Caching vcpkg and Conan Packages on GitHub Actions

vcpkg and Conan rebuild from source on GitHub Actions until the binary cache outlives the job. Restore the archives, key them safely, and ban source builds.

Last verified:

vcpkg and Conan both answer a dependency request by looking for a prebuilt binary in a local directory first, and on GitHub Actions that directory is created with the job and destroyed with it, so every run compiles the whole tree from source. The fix has three parts: put each tool's binary store on a path a cache action can restore and save, key that path on the inputs the tool already hashes, and fail the job when a package would be compiled from source anyway.

This guide covers where each tool keeps its binaries, which key inputs make a hit safe, a workflow that restores the archives and refuses to fall back to a source build, and a time model that prices source builds against binary cache hits for a stated dependency count. It sits under the persistent caches for GitHub Actions runs hub.

Diagnosis

Where the binaries live and what the key has to cover

Both tools already compute a content hash for every package. The cache key only has to cover the coarse inputs that decide which hashes are in play at all, so the restored archive belongs to the same compiler, the same platform, and the same dependency set.

Package managerBinary storePath on a runnerKey inputs that make a hit safe
vcpkgBinary cache archives, one per port ABI hash$HOME/.cache/vcpkg/archives on Linux, %LOCALAPPDATA%\vcpkg\archives on Windows, or the path given to the files provider in VCPKG_BINARY_SOURCESHash of vcpkg.json and vcpkg-configuration.json, the builtin-baseline commit, the triplet, the runner image label
Conan 2Package folders under the cache home~/.conan2 by default, or $CONAN_HOMEHash of conan.lock and the profile files, plus the settings in the host profile: os, arch, compiler, compiler.version, compiler.libcxx, build_type

The per package identity underneath is where correctness actually lives. vcpkg computes an ABI hash for each port from the port version and selected features, the portfile contents, the triplet settings, the compiler it detects, and the ABI hashes of that port's dependencies; the vcpkg binary caching documentation describes the providers and the default files behavior. Conan computes a package_id from the recipe reference and revision, the settings above, the options, and the resolved requirements. A stale archive cannot be served to a different compiler by accident, because the hash changes first.

That leaves the key doing one job: keeping the restored directory small and relevant. See what a cache key is for the general shape.

Confirm the run is compiling rather than downloading

Read the job log before editing anything. vcpkg prints a line per port telling you whether it restored from a binary cache or built, and the summary at the end reports how many packages were installed from cache. Force the detail out with:

vcpkg install --debug 2>&1 | tee vcpkg-install.log
grep -c "Restored from" vcpkg-install.log
grep -c "Building" vcpkg-install.log

On the Conan side, conan install . --build=missing reports every package it decided to build. Two commands give you the denominator and the actual decision:

conan graph info . --format=json | jq '[.graph.nodes[] | select(.ref != null)] | length'
conan install . --build=missing --format=json | jq -r '.graph.nodes[] | "\(.ref) \(.binary)"'

The binary field on each node reads Cache, Download, or Build. Count the Build entries. That count, and the wall clock of the install step, are the two inputs to the model at the end of this page.

Decide whether the transfer pays

The restore and the save are runner minutes, and a C++ dependency tree compresses poorly. Time both steps on a real run. WarpBuild CI observability correlates runner system metrics with GitHub Actions job logs, which separates an install step saturating every core on compilation from one sitting in a network transfer, and the Action Debugger opens a session on the live runner when a port keeps missing and the logs will not say why. Snapshot runners, remote Docker builders, and an MCP server sit on the same surface, and the snapshot path matters here once the archives plus the build trees outgrow what a transfer can carry.

Fix

1. Give vcpkg an explicit binary source inside the workspace. Set VCPKG_BINARY_SOURCES to clear;files,${{ github.workspace }}/.vcpkg-cache,readwrite. The clear term drops the default source so exactly one directory is in play, and readwrite lets the same run consume and populate it. Other providers exist, including a NuGet feed provider, and the choice does not change anything else on this page.

2. Pin the vcpkg baseline in the manifest. builtin-baseline in vcpkg.json fixes the registry commit that resolves port versions, so the ABI hashes stop moving between runs for reasons unrelated to your code. Without it, a port set can change under you and every archive in the cache becomes unreachable.

3. Point Conan at a lock file and a committed profile. Generate conan.lock with conan lock create ., commit it along with the profiles under .conan-profiles/, and pass both on every install. The lock file fixes the recipe revisions, and the profile fixes the settings that feed package_id.

4. Restore before the install step and save after it. WarpBuilds/cache@v1 is a drop-in replacement for actions/cache@v4 and is enabled by default on WarpBuild Linux runners; the inputs, the key matching, and the scoping rules are in the caching documentation. Use the split restore and save actions so the save can be gated on a successful build.

5. Ban the accidental source build. Add --only-binarycaching to vcpkg install and --build=never to conan install. Each one turns a silent 30 minute compile into a failed step with the name of the package that missed. Run that strict form on the default branch and on release workflows, and let pull requests use a permissive form so a contributor adding one dependency is not blocked.

6. Move to machine state when the archives outgrow the transfer. Once the binary cache plus the build trees runs to tens of gigabytes, restoring it costs more than it returns. Snapshot runners boot a later job from a captured VM image with the packages already on disk, which takes the transfer off the critical path. The compiler side of the same problem is covered in using ccache for C and C++ builds on GitHub Actions, and a GUI application that pulls its own dependencies through either tool alongside Qt is covered in Qt application builds on GitHub Actions.

Configuration

The workflow

name: deps

on:
  pull_request:
  push:
    branches: [main]

jobs:
  vcpkg:
    runs-on: warp-ubuntu-latest-x64-16x
    timeout-minutes: 60
    env:
      VCPKG_BINARY_SOURCES: clear;files,${{ github.workspace }}/.vcpkg-cache,readwrite
      VCPKG_DEFAULT_TRIPLET: x64-linux
    steps:
      - uses: actions/checkout@v4

      - name: Restore the vcpkg binary cache
        id: vcpkg-cache
        uses: WarpBuilds/cache/restore@v1
        with:
          path: ${{ github.workspace }}/.vcpkg-cache
          key: vcpkg-x64-linux-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json') }}-${{ github.sha }}
          restore-keys: |
            vcpkg-x64-linux-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json') }}-
            vcpkg-x64-linux-

      - name: Install dependencies from binaries only
        run: >
          vcpkg install
          --triplet x64-linux
          --only-binarycaching
          --x-manifest-root=.

      - name: Configure and build
        run: |
          cmake -S . -B build -G Ninja \
            -DCMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" \
            -DVCPKG_INSTALL_OPTIONS=--only-binarycaching
          cmake --build build --parallel 16

      - name: Save the vcpkg binary cache
        if: success() && github.ref == 'refs/heads/main'
        uses: WarpBuilds/cache/save@v1
        with:
          path: ${{ github.workspace }}/.vcpkg-cache
          key: ${{ steps.vcpkg-cache.outputs.cache-primary-key }}

  conan:
    runs-on: warp-ubuntu-latest-x64-16x
    timeout-minutes: 60
    steps:
      - uses: actions/checkout@v4

      - name: Restore the Conan package archive
        id: conan-cache
        uses: WarpBuilds/cache/restore@v1
        with:
          path: conan-cache.tgz
          key: conan-x64-linux-${{ hashFiles('conan.lock', '.conan-profiles/**') }}
          fail-on-cache-miss: ${{ github.ref == 'refs/heads/main' }}

      - name: Load the archive into the Conan cache
        if: steps.conan-cache.outputs.cache-hit == 'true'
        run: conan cache restore conan-cache.tgz

      - name: Install dependencies without building
        run: >
          conan install .
          --lockfile=conan.lock
          --profile:host=.conan-profiles/linux-x64
          --profile:build=.conan-profiles/linux-x64
          --build=never
          --output-folder=build

      - name: Build
        run: |
          cmake -S . -B build -G Ninja -DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake
          cmake --build build --parallel 16

      - name: Repack and save the Conan cache
        if: success() && github.ref == 'refs/heads/main'
        run: conan cache save "*:*" --file=conan-cache.tgz
      - if: success() && github.ref == 'refs/heads/main'
        uses: WarpBuilds/cache/save@v1
        with:
          path: conan-cache.tgz
          key: ${{ steps.conan-cache.outputs.cache-primary-key }}

Three inputs carry the guard. --only-binarycaching makes vcpkg exit with an error listing the ports that had no archive. --build=never makes Conan exit with an error naming the missing package_id. fail-on-cache-miss on the restore step stops the default branch from running at all when the entry is gone, which surfaces a broken key immediately rather than as a 30 minute job.

Key design

Put the manifest hash in the middle of the key and the commit SHA at the end. The commit term guarantees the primary key never matches, so restore-keys always resolves to the newest entry on the prefix and the save always writes a fresh entry. The manifest term gives the fallback two levels: same dependency set first, then any recent entry for the triplet.

Lead the key with the triplet or the profile name, never with runner.os alone. A matrix that spans them needs one key prefix per target: archives built for x64-linux cannot serve a job on warp-ubuntu-latest-arm64-16x, and a Windows job on warp-windows-latest-x64-8x needs its own prefix again. Both tools would refuse the mismatched package anyway, so a shared prefix costs transfer time and returns nothing.

Save on pushes to the default branch and restore everywhere. A pull request that adds one port then compiles that port and throws the archive away, which is the correct trade: the alternative writes an entry per pull request and pays storage for entries read once.

Retention and fees

A cache entry expires 7 days after its last use, and it can be deleted at any time from the action or from the console. Cache storage bills at $0.20 per GB-month and every write or restore bills at $0.0001 per operation on hosted runners; on BYOC runners, cache storage and operations are included. Entries are scoped to the key, the version, and the branch, so a feature branch reads the default branch entry through the fallback prefix and cannot overwrite it.

Cost or Time Model

Runner rates come from the pricing page and the cloud runners documentation. GitHub list prices come from the Actions billing reference, checked on 2026-08-13. Replace the build inputs with the counts you collected in the diagnosis section.

Assumptions

  • A vcpkg manifest whose closure resolves 62 ports, producing 1.2 GB of archives.
  • A Conan graph of 48 packages, producing a 0.9 GB archive from conan cache save.
  • Both jobs on warp-ubuntu-latest-x64-16x at $0.032 per minute.
  • Cold source builds: 34.0 minutes for the 62 vcpkg ports, 21.0 minutes for the 48 Conan packages.
  • Full binary hit: 2.0 minutes for vcpkg including restore, 1.4 minutes for Conan including restore and conan cache restore.
  • A baseline or lock file bump on one run in twenty rebuilds 6 of the 62 ports and 5 of the 48 packages, which adds 3.3 and 2.2 minutes on those runs.
  • 400 pull request builds per month.

Average dependency minutes per run work out to 2.17 for vcpkg (0.95 times 2.0 plus 0.05 times 5.3) and 1.51 for Conan.

Minutes and monthly cost

PathDependency minutes per runMonthly minutesMonthly cost
vcpkg, source build every run34.0013,600$435.20
vcpkg, binary cache2.17868$27.78
Conan, source build every run21.008,400$268.80
Conan, binary cache1.51604$19.33

Cache fees on top of the vcpkg path: 1.2 GB of storage at $0.20 per GB-month is $0.24, and 420 operations at $0.0001 each is $0.04. The cached vcpkg path lands at $28.06 against $435.20, and it returns 31.8 minutes of wall clock on every pull request, which is the number a reviewer actually feels.

The same minutes at GitHub-hosted list prices

warp-ubuntu-latest-x64-16x (16 vCPU, 64 GB) costs $0.032 per minute against $0.042 per minute for the 16-core Linux larger runner (16 vCPU, 64 GB): 24 percent lower list price. Holding the cached minute counts fixed, the 868 vcpkg minutes list at $27.78 against $36.46 on that GitHub-hosted shape, and the 604 Conan minutes list at $19.33 against $25.37.

Two levers stack, and they are worth separating when you build the case internally. Binary caching removes minutes, and the per minute list price sets what the remaining minutes cost. The first lever is the larger one here and you control it entirely from the workflow file.

Sizing the compile phase that follows the install step, along with link time and unity builds, is covered on C++ builds on GitHub Actions.

FAQ

How do I cache vcpkg packages on GitHub Actions?

Point VCPKG_BINARY_SOURCES at a directory inside the workspace with the files provider in readwrite mode, restore and save that directory with a cache action, and key it on the vcpkg.json manifest, the vcpkg-configuration.json baseline, the triplet, and the runner image. Then add --only-binarycaching so the job fails instead of quietly compiling a port from source.

Why does vcpkg rebuild a port even though the binary cache restored?

The ABI hash for that port changed. vcpkg hashes the port version and features, the portfile contents, the triplet, the detected compiler, and the ABI hashes of every dependency, so a baseline bump, a triplet edit, or a runner image that ships a different compiler build produces a new hash and a miss. Run vcpkg install with --debug and read the ABI info file written under buildtrees to see which input moved.

Should I cache the whole ~/.conan2 directory?

Prefer conan cache save and conan cache restore. The save command writes only the recipes and package binaries matching the pattern you give it into one archive, while the cache home also holds downloaded sources and tool state that make the entry larger and the key harder to reason about. Pair it with --build=never so a missing binary fails the job rather than starting a source build.

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.