The API Server and the Action Channel

Firecracker has no "run this VM" command line in the usual sense. You start the binary with an API socket, and then you configure and control the microVM by sending JSON over that socket. The machinery that turns those HTTP requests into changes to the running Vmm is the subject of this chapter: the HTTP server (built on the in-house micro-http crate) lives in the firecracker binary, parses a request into a ParsedRequest, converts it into a VmmAction (the control-plane RPC enum), ships it across the action channel to the VMM thread, where a PrebootApiController or RuntimeApiController executes it and returns an ApiResponse. This is the control-plane RPC pattern, and it is the same shape whether you are adding a drive or taking a snapshot.

After this chapter you will be able to: trace any API endpoint from raw socket bytes to a VmmAction variant and back to an HTTP status code; explain why the API server is in the binary and not in vmm; locate the request parser, the action enum, and both controllers on your checkout; and read the swagger spec as the source of truth for the surface.

Note: The API server is in the firecracker binary (src/firecracker/src/api_server/), not in the vmm crate. This is deliberate: vmm is a reusable VMM library that knows nothing about HTTP. The binary is what bolts a REST control plane onto it. Keep the boundary straight — it is a frequent source of confusion.


Where the pieces live

# The API server, request parsing, and the micro-http usage are in the binary.
rg -n "ApiServer|ParsedRequest|micro_http|HttpServer|ServerRequest|ServerResponse" src/firecracker/src/api_server/

# The action enum, the controllers, and the response types are in vmm.
rg -n "enum VmmAction|PrebootApiController|RuntimeApiController|enum VmmData|enum VmmActionError" src/vmm/src/rpc_interface.rs

# The contract: the OpenAPI / swagger spec.
find src/firecracker -name "firecracker.yaml" -path "*swagger*"
PieceCrate / path (locate, don't memorize)Role
ApiServersrc/firecracker/src/api_server/Owns the listener; loops accepting micro-http requests
ParsedRequestsrc/firecracker/src/api_server/A validated, typed request — method + URI + parsed body
VmmActionsrc/vmm/src/rpc_interface.rsThe control-plane RPC enum: one variant per logical operation
PrebootApiControllersrc/vmm/src/rpc_interface.rsExecutes actions before the microVM is started
RuntimeApiControllersrc/vmm/src/rpc_interface.rsExecutes actions after StartMicroVm
VmmData / VmmActionErrorsrc/vmm/src/rpc_interface.rsSuccess payload / typed error returned to the API thread
swagger specsrc/firecracker/swagger/firecracker.yamlThe authoritative description of every endpoint and schema

micro-http: the server underneath

Firecracker does not pull in a heavyweight HTTP framework. It uses micro-http, a tiny in-house HTTP server (its own org repo, also vendored/depended-on here) that does just enough HTTP/1.1 over a Unix domain socket to support the REST API. Minimal dependencies = minimal attack surface, which is the whole philosophy of the project. Find how it is driven:

rg -n "micro_http|HttpServer::new|incoming|requests|ServerRequest" src/firecracker/src/api_server/

The server loop is simple: accept a connection on the UDS, read a request, hand it to the request parser, get back a response, write it. The parser is where validation happens.


Step 1: bytes → ParsedRequest

The raw request — method, URI path, and JSON body — is parsed and validated into a ParsedRequest. Each endpoint (/boot-source, /drives/{id}, /machine-config, /actions, /snapshot/create, …) has a parser that deserializes the body into a typed config struct and rejects malformed input before it ever reaches the VMM thread. Locate the per-endpoint parsing:

rg -n "parse|try_from_request|fn from_body|serde_json::from" src/firecracker/src/api_server/
# The endpoint set, ground-truthed against the spec:
rg -n "boot-source|drives|machine-config|network-interfaces|actions|snapshot|vsock|balloon|mmds|cpu-config|entropy" src/firecracker/src/api_server/

Validation here is the API's first line of defence. A bad path, an unknown field, an out-of-range vcpu_count — all become an HTTP 400 with a structured error body, and the VMM thread never sees them.

Endpoint (selected)MethodsProduces a VmmAction like
/boot-sourcePUTConfigureBootSource
/drives/{id}PUT/PATCHInsertBlockDevice / UpdateBlockDevice
/machine-configGET/PUT/PATCHGetVmMachineConfig / UpdateVmConfiguration
/network-interfaces/{id}PUT/PATCHInsertNetworkDevice / UpdateNetworkInterface
/actionsPUTStartMicroVm / FlushMetrics / SendCtrlAltDel
/snapshot/create, /snapshot/loadPUTCreateSnapshot / LoadSnapshot
/vmPATCHPause / Resume

(Action variant names drift — verify against enum VmmAction on your branch. The endpoints are authoritative in firecracker.yaml.)


Step 2: ParsedRequest → VmmAction → the channel

The ParsedRequest becomes a VmmAction, which the API thread boxes as ApiRequest = Box<VmmAction> and sends over the mpsc channel; it then writes the wake-up eventfd so the VMM thread's epoll loop returns and drains the channel. The API thread blocks waiting for the ApiResponse = Box<Result<VmmData, VmmActionError>>, then renders it as HTTP.

flowchart LR
    Sock["UDS bytes"] --> MH["micro-http: ServerRequest"]
    MH --> PR["ParsedRequest (validated)"]
    PR --> VA["VmmAction variant"]
    VA --> CH["mpsc send Box<VmmAction>"]
    CH --> EF["write eventfd -> wake VMM epoll"]
    EF --> CTRL{microVM started?}
    CTRL -->|no| PRE["PrebootApiController.handle"]
    CTRL -->|yes| RUN["RuntimeApiController.handle"]
    PRE --> RESP["Box<Result<VmmData, VmmActionError>>"]
    RUN --> RESP
    RESP --> HTTP["micro-http: 200 / 204 / 400 / 4xx"]

This is the control-plane RPC pattern in full: a typed request enum, a channel, an eventfd doorbell, a typed response. It is worth internalizing because adding a new API action means touching every layer — a new VmmAction variant, its handling in the right controller, a parser in the API server, and an entry in the swagger spec. (You build exactly this in Level 3 Lab 3.3.)


Step 3: dispatch by lifecycle — the two controllers

The VMM thread routes the action by whether the microVM has been started. The boundary is StartMicroVm (triggered by PUT /actions {"action_type":"InstanceStart"}).

rg -n "PrebootApiController|RuntimeApiController|fn handle_preboot_request|fn handle_request|build_and_boot" src/vmm/src/rpc_interface.rs

PrebootApiController runs while the microVM is being configured. It mutates the accumulating configuration — VmResources (resources.rs) — as drives, network interfaces, machine config, and boot source arrive. When it receives StartMicroVm, it calls the builder (build_and_boot_microvm / build_microvm_for_boot) and, on success, the process transitions to the runtime phase. See the-boot-sequence.md for what the builder does.

RuntimeApiController runs after boot. It accepts only the runtime-legal subset of actions: pause, resume, create/load snapshot, flush metrics, send ctrl-alt-del, and PATCH-style live updates (drive path, rate limiters). Attempt a preboot-only action here — say, adding a brand-new drive — and you get a typed error, not a half-applied change.

PrebootApiControllerRuntimeApiController
WhenBefore InstanceStartAfter InstanceStart
MutatesVmResources (configuration)A running Vmm
Legal actionsconfigure boot-source/drives/net/machine-config, snapshot load, startpause/resume, snapshot create, flush metrics, ctrl-alt-del, live PATCH
Illegal action resulttyped VmmActionErrortyped VmmActionError

Step 4: VmmData/VmmActionError → HTTP

The controller returns Result<VmmData, VmmActionError>. VmmData carries any success payload (e.g. the machine config for a GET); VmmActionError is a typed error enum that the API thread maps to an HTTP status and a structured JSON body. Locate the mapping:

rg -n "enum VmmData|enum VmmActionError|impl From.*VmmActionError|StatusCode|to_response" src/vmm/src/rpc_interface.rs src/firecracker/src/api_server/

The typed-error design means failures are exhaustive and self-documenting: every way an action can fail is a variant, and the HTTP layer maps each to the right status. There are no stringly-typed errors leaking arbitrary text to the client.


Reading exercise

# 1. The server loop and request parsing.
rg -n "ApiServer|HttpServer|ServerRequest|ParsedRequest|micro_http" src/firecracker/src/api_server/

# 2. The action enum — read every variant.
rg -n "enum VmmAction" -A 60 src/vmm/src/rpc_interface.rs

# 3. The two controllers and the boot boundary.
rg -n "PrebootApiController|RuntimeApiController|StartMicroVm|build_and_boot|build_microvm_for_boot" src/vmm/src/rpc_interface.rs

# 4. The response/error types and the HTTP mapping.
rg -n "enum VmmData|enum VmmActionError|StatusCode|to_response" src/vmm/src/rpc_interface.rs src/firecracker/src/api_server/

# 5. The contract.
sed -n '1,80p' src/firecracker/swagger/firecracker.yaml

# 6. Drive it live (boot a microVM first, see Lab 1.3), then:
API=/tmp/firecracker.socket
curl -s --unix-socket $API http://localhost/machine-config
curl -s -X PUT --unix-socket $API --data '{"vcpu_count":99,"mem_size_mib":1}' http://localhost/machine-config

Answer:

  1. Why is the API server in the firecracker binary and not in the vmm crate? What does that buy you?
  2. Trace PUT /drives/rootfs end to end: name the type at each stage from socket bytes to HTTP status.
  3. What exactly happens at the eventfd step, and why is it needed in addition to the mpsc channel?
  4. What flips dispatch from the PrebootApiController to the RuntimeApiController? Give one action that is legal in each and illegal in the other.
  5. Why is VmmActionError a typed enum rather than a string? What does that give the HTTP layer?
  6. You want to add a PUT /serial action. List every file/layer you must touch.

Common bugs and symptoms

SymptomRoot causeWhere to look
HTTP 400 on a valid-looking bodyAn unknown/renamed JSON field; serde rejects it at parse timethe endpoint's ParsedRequest parser
Action accepted but applied at wrong phaseRouted through the wrong controllerpreboot vs runtime dispatch in rpc_interface.rs
New endpoint returns 404Parser/route not registered in the API serverthe request-routing table in api_server/
Snapshot load rejectedLoadSnapshot attempted after boot (it is preboot-only)PrebootApiController legal-action set
Error returns 500 with a vague messageA failure path that isn't a typed VmmActionError variantadd/route the variant; HTTP status mapping
API works under --api-sock but config-file boot ignores a fieldConfig-file path parses sections directly, bypassing some validationthe --no-api / config-file parsing path

Validation: prove you understand this

  1. Draw the full path from a UDS byte stream to an HTTP response, naming the crate that owns each stage.
  2. Explain why HTTP, parsing, and the action enum are split across the firecracker binary and the vmm crate.
  3. Describe the action channel: the two mpsc directions, the eventfd, and the Box<...> types on each side.
  4. Contrast PrebootApiController and RuntimeApiController with the boot boundary and one legal action for each.
  5. Explain how a typed VmmActionError becomes an HTTP status and body, and why typed beats stringly-typed.
  6. Enumerate every layer you would modify to add one new API action, in order.

Next: KVM Fundamentals — the /dev/kvm layer beneath everything the control plane configures.