Can GitHub Actions Run an Android Emulator?
Yes. GitHub Actions runs a hardware-accelerated Android emulator on Linux x86-64 runners that expose /dev/kvm, once the KVM udev permission step is added.
Last verified:
Answer
Yes. GitHub Actions runs an Android emulator on Linux x86-64 runners, and the emulator runs with hardware acceleration whenever the runner exposes /dev/kvm to the job. On WarpBuild cloud runners you enable that by appending ;nested-virtualization.enabled=true to the runs-on label, then adding the documented KVM udev permission step before the emulator action (nested virtualization documentation, checked on 2026-08-13).
The label form is exact. The runner size comes first, then a semicolon, then the flag:
warp-ubuntu-latest-x64-4x;nested-virtualization.enabled=trueA complete instrumentation job looks like this:
name: android-instrumentation
on:
pull_request:
jobs:
instrumentation-tests:
runs-on: warp-ubuntu-latest-x64-4x;nested-virtualization.enabled=true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Confirm KVM is visible to the job
run: |
ls -l /dev/kvm
grep -c -E 'vmx|svm' /proc/cpuinfo
- uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
arch: x86_64
target: google_apis
disable-animations: true
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim
script: ./gradlew connectedDebugAndroidTestTwo things in that file carry the whole answer. The runs-on line asks for a runner provisioned on hardware that passes the virtualization extensions through to the guest, which is what makes /dev/kvm appear. The Enable KVM group perms step changes the device permissions so the runner user can open it.
Skipping the permission step is the most common failure, and it fails quietly. The device is present, the emulator still starts, and the suite still reports results, so the job looks healthy while every guest instruction is being emulated in software.
Pricing on WarpBuild is purely usage based.
Detail
What the permission step actually fixes
Once nested virtualization is on, /dev/kvm exists on the runner with the default device permissions crw-rw---- root:kvm. The runner user that executes workflow steps is outside that group, so opening the device fails (nested virtualization documentation).
The udev rule in the step above sets mode 0666 on the kvm kernel device and reloads the rules so the change applies to the live device rather than to the next boot. The step is a requirement on every GitHub-compatible runner, including GitHub-hosted larger runners, so it is portable across whichever runner your matrix lands on.
The failure signature is specific enough to grep for in a job log:
ProbeKVM: This user doesn't have permissions to use KVM (/dev/kvm).Disabling Linux hardware acceleration.- The emulator command line contains
-accel off.
Those three lines mean the emulator has fallen back to full software emulation of the guest ARM or x86 instruction stream. The same signature shows up in the common issues documentation as the first thing to check when an Android job that used to finish in minutes starts running long.
Add the permission step, rerun, and the three lines disappear. A useful hardening move is the ls -l /dev/kvm check in the example above: if the device is missing entirely, the label was ignored, which points at the runner class rather than at permissions.
Where nested virtualization is available
The support boundary matters before you write the label, because an unsupported runner class does not raise an error. The nested-virtualization.enabled=true label is silently ignored on runner classes that cannot provide it, and the job runs normally without hardware acceleration (nested virtualization documentation, checked on 2026-08-13).
| Runner class | Nested virtualization | How it is enabled |
|---|---|---|
| Cloud: Linux x86-64 | Yes | ;nested-virtualization.enabled=true on the runs-on label |
| Cloud: Linux ARM64 | No | Label ignored |
| Cloud: Windows | No | Label ignored |
| Cloud: macOS | No | Label ignored |
| BYOC: AWS, Linux x86-64 | Yes | Automatic when all instance types support it |
| BYOC: GCP, Linux x86-64 | Yes | Automatic when all instance types support it |
| BYOC: Azure, Linux x86-64 | Yes | Automatic on supported VM sizes |
| BYOC: ARM64 or Windows | No | Not available |
WarpBuild provides Linux x64, Linux ARM64, macOS, and Windows runners, and emulator jobs use the Linux x64 line while the rest of a matrix stays wherever it already runs. A repository that builds an iOS target on macOS and an Android target on Linux keeps both legs in one workflow file and puts the flag on the Android leg only.
On BYOC runner sets the flag is unnecessary. Nested virtualization enables itself when every instance type selected for that runner set supports it, and it stays off when the selection mixes supporting and non-supporting types, so that job behavior stays consistent across the set. If you widened the instance list for capacity reasons and emulator jobs slowed down afterwards, that condition is the first place to look.
The Linux x86-64 catalog for emulator jobs
Every row is from the cloud runners documentation and the WarpBuild rate table, checked on 2026-08-13. The latest labels track Ubuntu 24.04; the same five sizes exist as warp-ubuntu-2604-x64-* on Ubuntu 26.04 at the same rates.
| Runner label | vCPU | RAM | Storage | Per minute | Alias |
|---|---|---|---|---|---|
| warp-ubuntu-latest-x64-2x | 2 | 8 GB | 150GB SSD | $0.004 | warp-ubuntu-2404-x64-2x |
| warp-ubuntu-latest-x64-4x | 4 | 16 GB | 150GB SSD | $0.008 | warp-ubuntu-2404-x64-4x |
| warp-ubuntu-latest-x64-8x | 8 | 32 GB | 150GB SSD | $0.016 | warp-ubuntu-2404-x64-8x |
| warp-ubuntu-latest-x64-16x | 16 | 64 GB | 150GB SSD | $0.032 | warp-ubuntu-2404-x64-16x |
| warp-ubuntu-latest-x64-32x | 32 | 128 GB | 150GB SSD | $0.064 | warp-ubuntu-2404-x64-32x |
The flag appends to any of them, so warp-ubuntu-2604-x64-8x;nested-virtualization.enabled=true is valid in the same way the 4x form is.
Sizing for an emulator job is different from sizing a compile job, because three workloads share the machine: the emulator process, the Gradle daemon, and the app under test. A 2 vCPU runner leaves the emulator competing with Gradle for cores during the test run, which shows up as AVD boot timeouts rather than as slow compilation. The 4 vCPU size is the practical floor, and 8 vCPU is the step to take when a job boots more than one AVD or when the instrumentation suite is large. The Recommendations view in runner observability reports right-sizing suggestions per job from real utilization, which beats guessing from job duration alone. The runner size answer page covers the general method.
What an emulator suite costs
WarpBuild rates below come from the cloud runners documentation. GitHub rates come from the GitHub Actions billing reference and the GitHub-hosted runner specifications, checked on 2026-08-13.
| WarpBuild label | Shape | WarpBuild per minute | GitHub-hosted equivalent | GitHub per minute | List price difference |
|---|---|---|---|---|---|
| warp-ubuntu-latest-x64-2x | 2 vCPU, 8 GB | $0.004 | ubuntu-latest on private repositories | $0.006 | 33 percent lower list price |
| warp-ubuntu-latest-x64-4x | 4 vCPU, 16 GB | $0.008 | 4-core Linux larger runner | $0.012 | 33 percent lower list price |
| warp-ubuntu-latest-x64-8x | 8 vCPU, 32 GB | $0.016 | 8-core Linux larger runner | $0.022 | 27 percent lower list price |
| warp-ubuntu-latest-x64-16x | 16 vCPU, 64 GB | $0.032 | 16-core Linux larger runner | $0.042 | 24 percent lower list price |
| warp-ubuntu-latest-x64-32x | 32 vCPU, 128 GB | $0.064 | 32-core Linux larger runner | $0.082 | 22 percent lower list price |
The 2 vCPU row compares GitHub's private-repository rate, which is the rate paying teams run against. GitHub gives public repositories a larger free shape.
Worked model: one Android repository. A team merges enough pull requests to run 400 instrumentation jobs a month. Each job takes 14 minutes: roughly 3 minutes of Gradle work, 2 minutes of AVD boot, and 9 minutes of tests. That is 5,600 minutes on a 4 vCPU runner.
warp-ubuntu-latest-x64-4xat $0.008 per minute: 5,600 x $0.008 = $44.80 per month.- The GitHub 4-core Linux larger runner at $0.012 per minute: 5,600 x $0.012 = $67.20 per month.
- Difference: $22.40 per month, $268.80 over twelve months, which is 33 percent lower list price (GitHub pricing, checked 2026-08-13).
- The $10 in signup credits covers the first $10 of that bill, so month one lands at $34.80 out of pocket at this volume.
Minutes are the other half of the bill, and this is where the permission step pays for itself twice. Software emulation stretches the AVD boot and the test phase of every one of those 400 jobs, so a missing four-line udev step inflates both the wall clock and the invoice on the same jobs.
Keeping emulator jobs stable once acceleration works
Hardware acceleration removes the largest source of Android job slowness and leaves a shorter list of flake sources behind. Four changes cover most of what remains.
Cache the AVD. Creating the AVD and booting it cold on every run is the longest fixed cost in the job. Cache ~/.android/avd and ~/.android/adb* keyed by API level, then run the emulator action twice: once with emulator-options: -no-snapshot-load to generate and save the snapshot on a cache miss, and once for the test run.
- uses: actions/cache@v4
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-34-x86_64
- name: Create AVD snapshot
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
arch: x86_64
target: google_apis
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim
script: echo "snapshot generated"Turn animations off. disable-animations: true stops Espresso from racing window transitions, which is the classic source of a test that passes locally and fails on a runner.
Pin the API level and the image target. api-level, arch, and target together determine which system image is downloaded. Leaving any of them implicit lets the image drift under you between runs.
Split the matrix and keep both legs reporting. Run one job per API level with fail-fast: false so a failure on API 30 still lets you see the API 34 result in the same run. Every leg carries the same label suffix, since the flag applies per job.
If a job still fails intermittently after all of that, the failure is usually in the test code or in the app rather than in the runner. The flaky jobs guide walks through isolating the two. Job-level observability data is the fastest way to tell a slow emulator boot apart from a slow test suite.
Related Questions
Why do my Android emulator tests still flake after KVM is enabled?
Check the emulator logs for ProbeKVM permission errors and for -accel off in the emulator command line, because those two signals mean the job fell back to software emulation. Once hardware acceleration is confirmed, the remaining flake sources are AVD boot timeouts, animations left enabled, and tests that race the package installer. The flaky jobs guide covers how to separate a runner problem from a test problem.
Which runner size should I use for Android instrumentation tests?
Start at warp-ubuntu-latest-x64-4x with 4 vCPU and 16 GB at $0.008 per minute, because the emulator, the Gradle daemon, and the app under test share the machine. Move to warp-ubuntu-latest-x64-8x with 8 vCPU and 32 GB at $0.016 per minute when you boot more than one AVD per job or run a large instrumentation suite. Price both against your own job minutes on the pricing page.
Does nested virtualization work on BYOC runners?
Yes on AWS, GCP, and Azure Linux x86-64 runner sets, and it enables itself with no label when every instance type selected for the runner set supports it. Mixing supporting and non-supporting instance types leaves it off for the whole runner set, so pin the instance list to types that support it. The nested virtualization documentation lists the per-cloud behavior.
Can I run the Android emulator on ARM64, Windows, or macOS runners?
No. Nested virtualization is a Linux x86-64 feature on cloud runners, and the nested-virtualization.enabled=true label is silently ignored on ARM64, Windows, and macOS. Route emulator jobs to an x86-64 label and keep the rest of the matrix wherever it already runs.
Set up the job with the Android emulator testing solution, fit it into a full pipeline with the Android on GitHub Actions solution, and price the runner minutes on the pricing page.
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.