<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Adwitiya Trivedi</title>
    <description>The latest articles on DEV Community by Adwitiya Trivedi (@adwitiya).</description>
    <link>https://dev.to/adwitiya</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3828187%2F1ce0aa16-8732-4e76-9c12-9434c1cb097a.png</url>
      <title>DEV Community: Adwitiya Trivedi</title>
      <link>https://dev.to/adwitiya</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/adwitiya"/>
    <language>en</language>
    <item>
      <title>How I built sandboxes that boot in 28ms using Firecracker snapshots</title>
      <dc:creator>Adwitiya Trivedi</dc:creator>
      <pubDate>Mon, 16 Mar 2026 22:38:05 +0000</pubDate>
      <link>https://dev.to/adwitiya/how-i-built-sandboxes-that-boot-in-28ms-using-firecracker-snapshots-i0k</link>
      <guid>https://dev.to/adwitiya/how-i-built-sandboxes-that-boot-in-28ms-using-firecracker-snapshots-i0k</guid>
      <description>&lt;p&gt;A deep-dive into building a sandbox orchestrator that gives AI agents their own isolated machines. Firecracker microVMs, snapshot restore, and why 28ms matters.&lt;br&gt;
tags: go, opensource, ai, devops&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx9lv8sig5pd28ptnmf1u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx9lv8sig5pd28ptnmf1u.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I've been building AI agents that generate and execute code. The agents write Python scripts, run data analysis, generate charts, process files. Standard stuff in 2026.&lt;/p&gt;

&lt;p&gt;The problem I kept hitting: where does that code actually run?&lt;/p&gt;

&lt;p&gt;I tried Docker. It works, but containers share the host kernel. When the runc CVEs dropped in 2024-2025 (CVE-2024-21626, then three more in 2025), I started thinking harder about what "isolation" actually means when an AI is writing arbitrary code on my machine.&lt;/p&gt;

&lt;p&gt;I tried E2B. Great product, but my data was leaving my machine. For an internal tool processing company data, that was a non-starter.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://github.com/DohaerisAI/forgevm" rel="noopener noreferrer"&gt;ForgeVM&lt;/a&gt;. A single Go binary that orchestrates isolated sandboxes. This article is about the hardest part: getting Firecracker microVMs to boot in 28ms.&lt;/p&gt;


&lt;h2&gt;
  
  
  What Firecracker actually is
&lt;/h2&gt;

&lt;p&gt;Firecracker is AWS's microVM manager. It's what powers Lambda and Fargate. Open source, written in Rust, runs on KVM.&lt;/p&gt;

&lt;p&gt;The key insight: Firecracker is not QEMU. QEMU emulates an entire PC with hundreds of devices. Firecracker emulates exactly 4 devices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;virtio-block (disk)&lt;/li&gt;
&lt;li&gt;virtio-net (network)&lt;/li&gt;
&lt;li&gt;serial console&lt;/li&gt;
&lt;li&gt;1-button keyboard (just to stop the VM)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it. No USB, no GPU, no sound card, no PCI bus. This minimal device model is why it's fast and why the attack surface is tiny.&lt;/p&gt;

&lt;p&gt;Each Firecracker microVM gets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Its own Linux kernel&lt;/li&gt;
&lt;li&gt;Its own root filesystem&lt;/li&gt;
&lt;li&gt;Its own network namespace&lt;/li&gt;
&lt;li&gt;Communication with the host via vsock (virtio socket, not TCP)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A guest exploit can't reach the host because there's a hardware boundary (KVM) between them. Compare that to Docker where a kernel vulnerability affects every container on the host.&lt;/p&gt;


&lt;h2&gt;
  
  
  The cold boot problem
&lt;/h2&gt;

&lt;p&gt;Here's the thing though. Booting a Firecracker microVM from scratch takes about 1 second. That includes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Firecracker process starts (~50ms)&lt;/li&gt;
&lt;li&gt;Load kernel into memory (~100ms)&lt;/li&gt;
&lt;li&gt;Kernel boots, init runs (~500ms)&lt;/li&gt;
&lt;li&gt;Guest agent starts and signals ready (~200ms)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;1 second is fine for long-running workloads. It's not fine when your AI agent needs to run &lt;code&gt;print(1+1)&lt;/code&gt; and return the result in a chat interface. Users notice 1 second of latency.&lt;/p&gt;

&lt;p&gt;I needed sub-100ms. Ideally sub-50ms.&lt;/p&gt;


&lt;h2&gt;
  
  
  The snapshot trick
&lt;/h2&gt;

&lt;p&gt;Firecracker supports snapshotting a running VM's complete state to disk. This includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Full memory contents&lt;/strong&gt; (the entire RAM, written to a file)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU register state&lt;/strong&gt; (instruction pointer, stack pointer, all registers)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Device state&lt;/strong&gt; (virtio queues, serial port state)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you restore from a snapshot, Firecracker doesn't boot a kernel. It doesn't run init. It doesn't start your agent. It memory-maps the snapshot file, loads the CPU state, and resumes execution from exactly where it left off.&lt;/p&gt;

&lt;p&gt;The VM doesn't know it was ever stopped. From the guest's perspective, time just skipped forward.&lt;/p&gt;

&lt;p&gt;Here's what this looks like in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# First spawn (cold boot) - ~1 second
1. Start Firecracker process
2. Boot kernel + rootfs
3. Wait for guest agent to signal ready
4. Pause the VM
5. Snapshot memory + CPU + devices to disk
6. Resume the VM, hand it to the user

# Every subsequent spawn - ~28ms
1. Copy the snapshot files (copy-on-write, nearly instant)
2. Start new Firecracker process with --restore-from-snapshot
3. VM resumes exactly where the snapshot was taken
4. Guest agent is already running, already ready
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 28ms breaks down roughly as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~5ms: Firecracker process startup&lt;/li&gt;
&lt;li&gt;~8ms: mmap the memory snapshot file&lt;/li&gt;
&lt;li&gt;~10ms: restore CPU and device state&lt;/li&gt;
&lt;li&gt;~5ms: vsock reconnection and ready signal&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How I implemented it in Go
&lt;/h2&gt;

&lt;p&gt;ForgeVM's Firecracker provider manages the snapshot lifecycle. Here's the simplified flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;FirecrackerProvider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;opts&lt;/span&gt; &lt;span class="n"&gt;SpawnOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Check if we have a snapshot for this image&lt;/span&gt;
    &lt;span class="n"&gt;snap&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getSnapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;snap&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// Fast path: restore from snapshot (~28ms)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;restoreFromSnapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// Slow path: cold boot + create snapshot (~1s)&lt;/span&gt;
    &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;coldBoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// Wait for guest agent to be ready&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;waitForAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// Pause VM and snapshot&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pauseVM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;createSnapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resumeVM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The snapshot files are per-image. First time someone spawns &lt;code&gt;python:3.12&lt;/code&gt;, it cold-boots, snapshots, and every subsequent &lt;code&gt;python:3.12&lt;/code&gt; spawn restores in 28ms. Different images get different snapshots.&lt;/p&gt;

&lt;h3&gt;
  
  
  The copy-on-write detail
&lt;/h3&gt;

&lt;p&gt;You can't share a single snapshot file across multiple running VMs because each VM writes to memory. The solution is copy-on-write:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The base snapshot is read-only&lt;/li&gt;
&lt;li&gt;Each new VM gets a CoW overlay for both the memory file and the rootfs&lt;/li&gt;
&lt;li&gt;Writes go to the overlay, reads fall through to the base&lt;/li&gt;
&lt;li&gt;On destroy, delete the overlay. Base snapshot stays pristine.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This means 50 running VMs from the same snapshot share most of their memory pages. Only the pages that each VM actually wrote are unique. Memory efficient.&lt;/p&gt;




&lt;h2&gt;
  
  
  The guest agent
&lt;/h2&gt;

&lt;p&gt;Each Firecracker VM runs a custom agent binary (&lt;code&gt;forgevm-agent&lt;/code&gt;) as PID 1. The agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Listens on vsock for commands from the host&lt;/li&gt;
&lt;li&gt;Executes commands via &lt;code&gt;os/exec&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Handles file read/write/list/delete operations&lt;/li&gt;
&lt;li&gt;Streams stdout/stderr back to the host in real-time&lt;/li&gt;
&lt;li&gt;Uses a length-prefixed JSON protocol over the vsock connection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The protocol is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[4 bytes: message length][JSON payload]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"exec"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"python3 /app/main.py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"workdir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/workspace"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response (streamed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stdout"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hello world&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"exit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;vsock is important here. It's a virtio socket, not TCP/IP. The guest has no network stack visible to the host. There's no IP address, no port, no routing. Just a direct kernel-to-kernel channel. This eliminates an entire class of network-based attacks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why not just Docker?
&lt;/h2&gt;

&lt;p&gt;I actually built a Docker provider too. ForgeVM has a provider interface, and Docker is one of the backends. Here's the honest comparison:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docker containers:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Boot: ~200-500ms&lt;/li&gt;
&lt;li&gt;Isolation: Linux namespaces + cgroups + seccomp&lt;/li&gt;
&lt;li&gt;Attack surface: Shared host kernel. Every syscall from the container hits the real kernel.&lt;/li&gt;
&lt;li&gt;KVM needed: No&lt;/li&gt;
&lt;li&gt;Runs on: Linux, Mac, Windows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Firecracker microVMs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Boot: ~28ms (snapshot) / ~1s (cold)&lt;/li&gt;
&lt;li&gt;Isolation: KVM hardware virtualization. Separate kernel per sandbox.&lt;/li&gt;
&lt;li&gt;Attack surface: Minimal VMM with 4 devices. Guest kernel is a separate kernel.&lt;/li&gt;
&lt;li&gt;KVM needed: Yes&lt;/li&gt;
&lt;li&gt;Runs on: Linux with /dev/kvm&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;gVisor (via Docker provider with runsc runtime):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Boot: ~300-800ms&lt;/li&gt;
&lt;li&gt;Isolation: User-space kernel intercepts syscalls. ~70 host syscalls exposed.&lt;/li&gt;
&lt;li&gt;Attack surface: Much smaller than Docker, larger than Firecracker.&lt;/li&gt;
&lt;li&gt;KVM needed: No&lt;/li&gt;
&lt;li&gt;Runs on: Linux&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In ForgeVM, you switch between these with one config change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;firecracker"&lt;/span&gt;  &lt;span class="c1"&gt;# or "docker"&lt;/span&gt;
  &lt;span class="na"&gt;docker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;runc"&lt;/span&gt;        &lt;span class="c1"&gt;# or "runsc" for gVisor&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same API. Same SDKs. Same pool mode. Different isolation level.&lt;/p&gt;

&lt;p&gt;For development, I use Docker (runs on my Mac). For production, Firecracker. The application code doesn't know or care which provider is active.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pool mode: the resource trick
&lt;/h2&gt;

&lt;p&gt;This is the part I'm most proud of and it has nothing to do with Firecracker specifically.&lt;/p&gt;

&lt;p&gt;Traditional sandbox tools: 1 user = 1 VM (or container). If you have 100 concurrent users, you need 100 VMs. At 512MB each, that's 50GB of RAM just for sandboxes.&lt;/p&gt;

&lt;p&gt;ForgeVM's pool mode: 1 VM serves up to N users. Each user gets a logical "sandbox" with its own workspace directory (&lt;code&gt;/workspace/{sandbox-id}/&lt;/code&gt;). The orchestrator:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Routes all exec calls to the shared VM but sets &lt;code&gt;WorkDir&lt;/code&gt; to the user's workspace&lt;/li&gt;
&lt;li&gt;Rewrites all file paths through &lt;code&gt;scopedPath()&lt;/code&gt; to prevent directory traversal&lt;/li&gt;
&lt;li&gt;Tracks user count per VM and creates new VMs when capacity is full&lt;/li&gt;
&lt;li&gt;Destroys VMs only when all users have left
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// scopedPath prevents user A from accessing user B's workspace&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;scopedPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vmID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sandboxID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;vmID&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;  &lt;span class="c"&gt;// 1:1 mode, no scoping&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;"/workspace/"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;sandboxID&lt;/span&gt;
    &lt;span class="n"&gt;cleaned&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Clean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;cleaned&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;  &lt;span class="c"&gt;// traversal attempt, return base&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cleaned&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;100 users, 20 VMs instead of 100. 60% less infrastructure.&lt;/p&gt;

&lt;p&gt;The security trade-off is real: pool mode gives you directory-level isolation, not kernel-level. Users in the same VM share a kernel. For internal tools where you trust the users but want to isolate the AI-generated code from the host, this is fine. For multi-tenant public platforms, you'd want the optional per-user UID and PID namespace hardening on top.&lt;/p&gt;




&lt;h2&gt;
  
  
  Numbers
&lt;/h2&gt;

&lt;p&gt;Some benchmarks from my development machine (AMD Ryzen 7, 32GB RAM, NVMe SSD):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Firecracker cold boot&lt;/td&gt;
&lt;td&gt;~1.1s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firecracker snapshot restore&lt;/td&gt;
&lt;td&gt;~28ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker container start (alpine)&lt;/td&gt;
&lt;td&gt;~180ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker container start (python:3.12)&lt;/td&gt;
&lt;td&gt;~450ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exec "echo hello" (Firecracker)&lt;/td&gt;
&lt;td&gt;~3ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exec "echo hello" (Docker)&lt;/td&gt;
&lt;td&gt;~8ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exec "python3 -c 'print(1)'" (Firecracker)&lt;/td&gt;
&lt;td&gt;~45ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File write 1MB (Firecracker, vsock)&lt;/td&gt;
&lt;td&gt;~12ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File write 1MB (Docker, tar copy)&lt;/td&gt;
&lt;td&gt;~25ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sandbox destroy (Firecracker)&lt;/td&gt;
&lt;td&gt;~15ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sandbox destroy (Docker)&lt;/td&gt;
&lt;td&gt;~50ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Firecracker exec latency is lower because vsock is a direct kernel channel, while Docker exec creates a new exec instance and attaches via the Docker daemon.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Start with Docker, not Firecracker.&lt;/strong&gt; I built the Firecracker provider first because I was excited about 28ms boots. But 80% of people trying ForgeVM don't have KVM available (Mac users, CI/CD, cloud VMs without nested virt). The Docker provider should have been day one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The guest agent protocol should have been gRPC, not custom JSON.&lt;/strong&gt; The length-prefixed JSON protocol works fine but I'm essentially maintaining a custom RPC framework. gRPC over vsock would have given me streaming, error codes, and code generation for free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pool mode security should have been built-in from the start.&lt;/strong&gt; The directory-level isolation works, but per-user UIDs and PID namespace isolation should be default-on, not optional. I'm retrofitting this now.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/DohaerisAI/forgevm &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;forgevm
./scripts/setup.sh
./forgevm serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;forgevm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:7423&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python:3.12&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;print(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;hello from a 28ms sandbox&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MIT licensed. Single binary. No telemetry. No cloud.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/DohaerisAI/forgevm" rel="noopener noreferrer"&gt;github.com/DohaerisAI/forgevm&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you made it this far and found this useful, a star on GitHub genuinely helps with discoverability. Happy to answer questions in the comments about the Firecracker internals, the provider architecture, or the pool mode design.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>agents</category>
      <category>linux</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
