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
firecrackerbinary (src/firecracker/src/api_server/), not in thevmmcrate. This is deliberate:vmmis 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*"
| Piece | Crate / path (locate, don't memorize) | Role |
|---|---|---|
ApiServer | src/firecracker/src/api_server/ | Owns the listener; loops accepting micro-http requests |
ParsedRequest | src/firecracker/src/api_server/ | A validated, typed request — method + URI + parsed body |
VmmAction | src/vmm/src/rpc_interface.rs | The control-plane RPC enum: one variant per logical operation |
PrebootApiController | src/vmm/src/rpc_interface.rs | Executes actions before the microVM is started |
RuntimeApiController | src/vmm/src/rpc_interface.rs | Executes actions after StartMicroVm |
VmmData / VmmActionError | src/vmm/src/rpc_interface.rs | Success payload / typed error returned to the API thread |
| swagger spec | src/firecracker/swagger/firecracker.yaml | The 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) | Methods | Produces a VmmAction like |
|---|---|---|
/boot-source | PUT | ConfigureBootSource |
/drives/{id} | PUT/PATCH | InsertBlockDevice / UpdateBlockDevice |
/machine-config | GET/PUT/PATCH | GetVmMachineConfig / UpdateVmConfiguration |
/network-interfaces/{id} | PUT/PATCH | InsertNetworkDevice / UpdateNetworkInterface |
/actions | PUT | StartMicroVm / FlushMetrics / SendCtrlAltDel |
/snapshot/create, /snapshot/load | PUT | CreateSnapshot / LoadSnapshot |
/vm | PATCH | Pause / 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.
PrebootApiController | RuntimeApiController | |
|---|---|---|
| When | Before InstanceStart | After InstanceStart |
| Mutates | VmResources (configuration) | A running Vmm |
| Legal actions | configure boot-source/drives/net/machine-config, snapshot load, start | pause/resume, snapshot create, flush metrics, ctrl-alt-del, live PATCH |
| Illegal action result | typed VmmActionError | typed 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:
- Why is the API server in the
firecrackerbinary and not in thevmmcrate? What does that buy you? - Trace
PUT /drives/rootfsend to end: name the type at each stage from socket bytes to HTTP status. - What exactly happens at the eventfd step, and why is it needed in addition to the mpsc channel?
- What flips dispatch from the
PrebootApiControllerto theRuntimeApiController? Give one action that is legal in each and illegal in the other. - Why is
VmmActionErrora typed enum rather than a string? What does that give the HTTP layer? - You want to add a
PUT /serialaction. List every file/layer you must touch.
Common bugs and symptoms
| Symptom | Root cause | Where to look |
|---|---|---|
| HTTP 400 on a valid-looking body | An unknown/renamed JSON field; serde rejects it at parse time | the endpoint's ParsedRequest parser |
| Action accepted but applied at wrong phase | Routed through the wrong controller | preboot vs runtime dispatch in rpc_interface.rs |
| New endpoint returns 404 | Parser/route not registered in the API server | the request-routing table in api_server/ |
| Snapshot load rejected | LoadSnapshot attempted after boot (it is preboot-only) | PrebootApiController legal-action set |
| Error returns 500 with a vague message | A failure path that isn't a typed VmmActionError variant | add/route the variant; HTTP status mapping |
API works under --api-sock but config-file boot ignores a field | Config-file path parses sections directly, bypassing some validation | the --no-api / config-file parsing path |
Validation: prove you understand this
- Draw the full path from a UDS byte stream to an HTTP response, naming the crate that owns each stage.
- Explain why HTTP, parsing, and the action enum are split across the
firecrackerbinary and thevmmcrate. - Describe the action channel: the two mpsc directions, the eventfd, and the
Box<...>types on each side. - Contrast
PrebootApiControllerandRuntimeApiControllerwith the boot boundary and one legal action for each. - Explain how a typed
VmmActionErrorbecomes an HTTP status and body, and why typed beats stringly-typed. - 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.