If you build a .NET application using GitHub Actions, you may notice that every workflow run spends time downloading NuGet packages. This can make your CI pipeline slower, especially when your project has many dependencies.
For example, your application may use Entity Framework Core, logging libraries, testing packages, and several other NuGet dependencies. Downloading them again and again adds unnecessary work to every build.
GitHub Actions provides caching features that can help. By caching NuGet packages, you can reuse packages downloaded during earlier workflow runs instead of downloading them all over again.
In this article, you will learn how to cache NuGet packages in GitHub Actions, configure a workflow for a .NET project, and avoid common mistakes that can make caching ineffective.
Why NuGet Package Caching Matters
When you build a .NET project, the dotnet restore command downloads the packages required by your project.
For example, a project file might contain these dependencies:
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore"
Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer"
Version="9.0.0" />
<PackageReference Include="xunit"
Version="2.9.2" />
</ItemGroup>These packages are stored in the NuGet global packages folder on the build machine.
On a local development machine, the packages often remain available between builds. As a result, restoring the same dependencies usually requires less work after the first run.
GitHub-hosted runners are different. Each new job normally starts on a fresh virtual machine, so packages downloaded during one job are not automatically available to the next job.
Without caching, a workflow may repeatedly download the same dependencies.
Caching lets GitHub Actions save the package folder and restore it in a later run when the cache key matches.
The result can be faster dependency restoration and shorter build times.
How NuGet Caching Works in GitHub Actions
The process is straightforward:
GitHub Actions checks whether a matching cache exists.
If a cache is available, the runner restores the cached NuGet packages.
The
dotnet restorecommand checks the project's dependencies and downloads anything missing.The application is built and tested.
When the cache is saved, later workflow runs can reuse it.
Caching does not replace dotnet restore. The restore command still checks that the required packages are available and that the project can be restored correctly.
This is important because your dependencies may change when you update a package version or modify a project file.
A good cache configuration should reuse existing packages without preventing the build from getting the dependencies it actually needs.
Step 1: Find the NuGet Package Cache Location
By default, NuGet stores downloaded packages in the global packages folder.
On Windows, the default location is usually:
%USERPROFILE%\.nuget\packagesOn Linux and macOS, it is usually:
~/.nuget/packagesGitHub-hosted Ubuntu runners commonly use a home directory such as /home/runner, so the package cache is often located at:
/home/runner/.nuget/packagesHowever, you should avoid hardcoding this path in your workflow. The location can change if the runner environment or NuGet configuration changes.
Instead, use the NUGET_PACKAGES environment variable to define a consistent location for both package restoration and caching.
For example:
env:
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packagesThis places the package folder inside the GitHub workspace.
Using a known location makes it easier to configure the cache action. Make sure all relevant restore and build steps use the same environment setting.
Step 2: Create a GitHub Actions Workflow
Create a workflow file in your repository:
.github/workflows/dotnet.ymlThe following example restores, builds, and tests a .NET application while caching NuGet packages.
name: .NET CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
env:
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- name: Cache NuGet packages
uses: actions/cache@v4
with:
path: ${{ env.NUGET_PACKAGES }}
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json', '**/*.csproj', '**/*.props', '**/*.targets', 'global.json', 'NuGet.config') }}
restore-keys: |
${{ runner.os }}-nuget-
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore --configuration Release
- name: Run tests
run: dotnet test --no-build --configuration ReleaseThis workflow is an example for a project using .NET 9. Change the SDK version and target framework to match your application.
Let's look at the important parts.
Set the package directory
env:
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packagesThis tells NuGet where to store the global package cache. The cache action uses the same path to save and restore packages.
Create a cache key
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json', '**/*.csproj', '**/*.props', '**/*.targets', 'global.json', 'NuGet.config') }}The cache key identifies the dependency setup associated with the cached packages.
It includes the operating system and hashes of important dependency configuration files. When those files change, GitHub Actions generates a different key.
Join the conversation! Your thoughts help the community grow.