All articles

How much slower is nested KVM? Measuring our SmolBox lab

A reader asked about nested virtualization overhead. Six paired tests showed why startup, guest execution and cleanup need separate clocks.

After I shared our article about testing SmolBox with nested KVM, a reader asked about performance. They had seen reports of large overhead from nested virtualization but had never measured it themselves.

Neither had I. We chose nesting so we could deliberately break a disposable worker environment and recover it from the physical host. It gave us a place to test full disks, worker failures and an unresponsive outer VM.

Their question was worth an experiment. On the same Linux machine, I compared a microVM running directly on the host with one running inside our lab VM.

Startup was much slower in the nested setup. The short CPU calculation had overlapping timings. Both observations matter when someone asks how much overhead nesting adds.

One question, several clocks

SmolBox manages disposable smolvm executions from Elixir. It tracks the job, collects its output and keeps cleanup visible until it finishes. SmolBox itself does not require nested virtualization.

For a program already running, overhead means extra time doing its work. For an application submitting a new job, it also includes creating the machine, starting it, transferring files and collecting the result. The capacity stays occupied until cleanup completes.

I measured those intervals separately. Otherwise, a large startup penalty could be mistaken for an equally large slowdown in every instruction the guest runs.

Same workload, an extra layer

Both configurations used the same smolvm binaries, approved Python image and microVM allocations. The Elixir controller ran beside its worker and used a private Unix socket in each case.

ComponentConfiguration
Physical Linux hostIntel Core i5-1135G7, 4 cores / 8 threads, 64 GiB RAM
Outer VM for the nested tests4 vCPUs, 8 GiB RAM
Each tested microVM1 vCPU, 512 MiB RAM
Software in both configurationsSmolBox 0.1.3, smolvm 1.16.0, Python 3.12.14
On the same physical Linux host, the direct setup runs an Elixir controller and smolvm worker beside a microVM. The nested setup puts the controller, worker and microVM inside an additional KVM guest. Both tested microVMs have one vCPU and 512 MiB RAM.
Only one job ran at a time. The outer lab VM stayed running during the direct samples, with no nested job executing.

I ran one warmup per configuration, then six measured pairs, alternating which configuration went first. Each sample created fresh machines. Images were already prepared and disk templates decompressed; downloads and dependency installation were outside the timings. Host caches were warm.

The workloads were deliberately small: a command that immediately exits, a fixed Python integer calculation, and a 36 MiB file write followed by fsync, a read and a hash check. A separate managed job followed the file workload through submission, output collection and verified cleanup.

Where the time went

These results compare two complete deployment configurations, which differ in more than their virtualization layers.

These are medians across six samples per configuration:

MeasurementDirectNested
Startup to the first successful command1.43 s8.04 s
Tiny command on a running VM, client round trip95.6 ms91.2 ms
CPU calculation inside the guest311 ms309 ms
File processing inside the guest179 ms442 ms
Managed file job through output collection2.41 s8.90 s
Managed file job through verified cleanup3.14 s9.32 s

Startup includes creation, start and the first successful command. Its median was about 5.6 times longer in the nested setup. The complete managed job, including cleanup, took about three times as long.

The CPU calculation tells a different story. Direct samples ranged from 287–353 ms; nested samples ranged from 284–337 ms. Those ranges overlap. The slightly lower nested median is not evidence that nesting makes computation faster. Timings for the tiny command overlap too.

Startup medians were 1.43 s directly and 8.04 s nested. The CPU medians were 311 ms and 309 ms; observed ranges were 287–353 ms and 284–337 ms. Each configuration has six samples. The CPU axis is zoomed, and the ranges overlap. These configurations also differ in kernel and storage; the figures do not isolate the effect of nesting. Animation timing is illustrative; plotted values come from the recorded measurements.
Startup medians were 1.43 s directly and 8.04 s nested. The CPU medians were 311 ms and 309 ms; observed ranges were 287–353 ms and 284–337 ms. Each configuration has six samples. The CPU axis is zoomed, and the ranges overlap. These configurations also differ in kernel and storage; the figures do not isolate the effect of nesting. Animation timing is illustrative; plotted values come from the recorded measurements. Watch the animation

There is another useful distinction: the CPU command's median client round trip was 479 ms directly and 568 ms nested. An application can wait longer even when the calculation measured inside its guest takes a similar time.

The file test's nested median was about 2.5 times longer, but direct results varied from 162–498 ms. That slow direct sample remains in the results. Six observations are enough for this first comparison, not a reliable estimate of how long jobs might take in the worst case.

What we can attribute to this setup

Neither worker nor the outer VM recorded CPU throttling during the measured intervals. Neither worker hit its memory limit or recorded an OOM kill. All twelve measured samples completed with correct results and verified cleanup.

Still, these were two deployment configurations, with differences beyond the extra virtualization layer. The physical host and outer guest used different Linux kernels. Nested storage passed through guest ext4 and a QEMU disk overlay; the direct worker used host ext4. Filesystem options differed as well.

CPU frequency remained dynamic, other host services stayed running, and we did not pin workloads to dedicated cores. The file test used buffered I/O and warm caches. We did not measure concurrency, network traffic or large working sets.

The full benchmark report records the ranges, resource controls, source identities and setup corrections. The raw sample data and benchmark scripts let readers inspect the measurements and adapt the method to their own environment.

Could another execution model change the result?

These measurements cover SmolBox's current approach: creating a fresh microVM from a prepared image for each execution. smolvm also supports branching running machines and restoring checkpoints. SmolBox 0.1.3 does not expose those operations, and we have not benchmarked them.

They are worthwhile candidates for another experiment. These results cannot tell us how much time they would save or what additional lifecycle management they would require.

Measure the wait your application cares about

Nesting served its purpose in our failure lab: we could damage the worker environment and recover it independently. The extra startup time was a tradeoff we accepted for those tests.

For an application, the relevant question is how often it pays that cost. A short job in a fresh VM and a long calculation inside a running one have very different timing profiles. Our small CPU test also says little about a workload that spends most of its time on I/O.

Start with your real workload. Measure startup, work, result collection and cleanup separately. The useful number is how long your application waits, and which part of that wait you can change.

Inspect the measurements.

The benchmark report includes every sample, configuration details and the procedure for repeating the comparison.

Read the method and results