Skip to main content
BlogAI

Perplexity SPACE: Firecracker Agent Sandbox, Warm Pools, E2B

How Perplexity's open-source SPACE runs agent tasks in Firecracker microVMs: warm pools, suspend and wake, an E2B-compatible API and red-team results.

Matthews Wong

Software Engineer

October 202612 min read
Perplexity SPACE: Firecracker Agent Sandbox, Warm Pools, E2B

Key Takeaway

Perplexity's open-source SPACE runs each agent task in a Firecracker microVM behind an HTTP API, with templates, suspend and wake, warm pools and an E2B-compatible wrapper. Its own red team recorded 0 of 108 VM escapes but 11 of 54 network bypasses with partial network access, since fixed. It needs a Linux KVM host.

On 15 October 2026 Perplexity open-sourced SPACE, the sandbox platform behind its Computer agent, and the repository's documentation is candid about what it does not guarantee. That candour is why I read the docs and Perplexity's own posts closely instead of relying on launch coverage.

This post is a close reading of the repository docs and three Perplexity posts: the announcement, the runtime design post and the red-team report Escaping SPACE Part I. I did not deploy SPACE, because its installer needs a Linux x86_64 host with KVM and I have not run it there. Every figure below comes from one of those pages, and I say whose measurement it is.

What SPACE is: five nouns and a control plane

SPACE stands for Sandboxed Platform for Agentic Code Execution. It is written in Rust, but you never embed it: applications talk to it over HTTP and server-sent events, and the repository's own first-sandbox example is a Python script that uses only the standard library. The vocabulary is small, and the architecture guide defines each term precisely.

ConceptMeaning in the architecture guide
TemplateThe starting filesystem and runtime configuration. Many sandboxes share one without changing it. A reference such as seed:latest selects a published release; a digest reference selects specific content.
SandboxAn isolated microVM with its own writable filesystem and lifecycle.
ProcessA program executing inside a sandbox. Processes in one sandbox share its environment and filesystem.
Command sessionConnection information and authorisation for one command. Creating the session assigns a process ID; attaching to it starts the process.
OperationA record tracking an asynchronous create, pause, suspend, wake or delete request.

The control flow follows from those definitions. The API records desired state in PostgreSQL, and controllers and worker services reconcile the running system towards it, so an accepted request says nothing about readiness. Management and output travel on separate paths: management goes through the API with a user-session or service bearer token, while command output flows through a data plane that accepts only a session-scoped attachment token. The two tokens are not interchangeable. Perplexity's design post describes the same shape: a stateless control plane, an unprivileged sandbox manager on each node, a node manager that is the single root process, and a space daemon inside the guest VM.

Running a first sandbox over the HTTP API

The sequence below is the repository's first_sandbox.py example reduced to curl. The endpoints, the runtime value fc and the field names sandbox_id, operation_id, session_id and attach.token come from that script and the first-sandbox guide; jq and uuidgen are my additions. Run it against the local Kind install from the installation guide.

# Assumes the local Kind install from docs/installation.md, which exports
# SPACE_API_URL, SPACE_DATA_PLANE_URL and SPACE_API_TOKEN (the login step).
H_AUTH="Authorization: Bearer $SPACE_API_TOKEN"

# 1. Create. The response is an operation, NOT a ready sandbox.
CREATE=$(curl -sS -X POST "$SPACE_API_URL/v1/sandboxes" \
  -H "$H_AUTH" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"template": "seed:latest", "runtime": "fc"}')
SBX=$(echo "$CREATE" | jq -r .sandbox_id)      # sbx_...
OP=$(echo "$CREATE"  | jq -r .operation_id)    # op_...

# 2. Wait for BOTH: operation state "succeeded" and sandbox state "running".
#    An accepted request is not a readiness signal.
curl -sS -H "$H_AUTH" "$SPACE_API_URL/v1/operations/$OP"  | jq -r .state
curl -sS -H "$H_AUTH" "$SPACE_API_URL/v1/sandboxes/$SBX" | jq -r .state

# 3. Create a command session. Commands are an argument ARRAY, not a shell
#    string; use sh -c yourself when you need pipes or expansion.
SESSION=$(curl -sS -X POST "$SPACE_API_URL/v1/sandboxes/$SBX/commands" \
  -H "$H_AUTH" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"command": ["sh", "-c", "uname -r; echo hello"], "timeout": "30s"}')
SID=$(echo "$SESSION" | jq -r .session_id)
ATTACH_TOKEN=$(echo "$SESSION" | jq -r .attach.token)

# 4. Attach on the DATA PLANE with the attachment token. Attaching is what
#    starts the process. The management token is rejected here on purpose.
curl -sS -N -H "Authorization: Bearer $ATTACH_TOKEN" \
  -H "Accept: text/event-stream" \
  "$SPACE_DATA_PLANE_URL/v1/sessions/$SID/events"
# data: lines are JSON. stdout/stderr carry data_base64; success is a
# process_exited event with exit_code 0, not a closed stream.

Two details trip people up. Waiting on the operation alone is not enough, because the operation's state describes the lifecycle request and the sandbox's state describes the environment, and neither is the command's exit status. And command output is a live stream, not log storage: reconnecting with Last-Event-ID produces resync_required rather than a replay, so the guide tells you to keep the transcript and to repeat a command only if repeating its effects is safe.

Pause, suspend and the six-hour idle clock

Agent sessions outlive any single request, so the lifecycle is where SPACE spends most of its documentation. There are two ways to stop a sandbox, and they keep different things.

StateExecutionWhat is retainedNext action
RunningProcesses can executeLive VM memory and the writable filesystemRun commands, pause, suspend or delete
PausedExecution is pausedVM memory stays resident and the filesystem stays on the worker. This is not a durable snapshot.Wake to resume
SuspendedNo guest executionA snapshot of VM memory, runtime state and root filesystem; active VM resources are releasedWake to restore

By default a sandbox created through the SPACE API suspends after six hours without access or command activity. The guide calls that an idle limit, not a maximum lifetime. Perplexity's design post explains how suspend works: the VM is paused, a full snapshot is taken and its artefacts are uploaded to object storage, and the snapshot becomes restorable only after every artefact has landed. Restore can then run on any node, which downloads the artefacts and reapplies the filesystem delta on the template. A scheduler also takes frequent disk snapshots and less frequent full snapshots, kept on the node, so a destructive command can be rolled back at the filesystem level.

Wait after wake. The guide says to repeat the running-state and process-readiness checks before issuing another command. Give every lifecycle mutation its own Idempotency-Key, and if you must retry after a timeout, reuse the same key and body rather than generating a new request.

Warm pools and base snapshots: where the create latency goes

Perplexity's design post gives the only latency numbers I found, and they are Perplexity's own: a launch-week comparison against its previous sandbox provider on production traffic, not a public benchmark. The announcement summarises the same result as sandbox creation being three to five times faster.

MetricPrevious providerSPACEChange
Median create latency185 ms60 ms3.1x faster
90th percentile create latency447 ms89 ms5.0x faster

The design post credits two mechanisms. On disk, SPACE uses btrfs: reflink copies share extents so only metadata is copied, and snapshots are atomic and store only changed data. In the cluster, a warm pool of pods holds common templates already on disk, a request binds to a matching pod, and its writable root filesystem is a copy-on-write clone. If no pod fits, the template is materialised on demand, and concurrent requests for the same image collapse into a single download. The repository docs describe the operator side, shown below.

# Kubernetes: a warm pool is ONE template release x ONE VM size.
space-admin-cli warm-pool create \
  --template my-template \
  --vcpu 2 \
  --memory-mib 4096 \
  --target-ready-slots 10 \
  --refill-per-tick 5 \
  --max-warming-slots 10
space-admin-cli warm-pool list     # wait until ready_slots reaches the target

# Native API creates ignore warm pools until admission-time claiming is on:
#   SPACE_NATIVE_CREATE_POOL_CLAIM_ENABLED=true   (API service)
#   feature flag space-native-create-pool-claim -> true
# E2B-compatible creates through ECW use pools with no extra setup.

# Single node, no Kubernetes: base snapshot first, then an optional node pool.
sudo /space/bin/space-tmplmgr base-snapshot --template my-template --state true
sudo /space/bin/space-tmplmgr create-pool --template my-template \
  --boot-mode snapshot --count 5
sudo /space/bin/space-sbxmgr create --runtime fc --template my-template \
  --boot-mode pool --blocking
# --boot-mode falls back WITHOUT an error: pool -> base-snapshot -> cold.
# Read the "Boot mode:" line in the output to see what actually ran.

The sharp edge is that every miss is silent. A create that finds no matching warm sandbox still succeeds with a normal cold boot, so a pool only helps if it matches. The warm-pools guide lists the causes: the pool is pinned to one release, so moving latest strands it; a create that sets vcpu and memory_mib must match the pool's size exactly; creates that request a custom disk_mib or file-change tracking always cold-boot; and a burst larger than ready_slots cold-boots until the pool refills. For a burst test, set the target to at least the burst size, and watch the warm_pool.create_path.duration_ms metric rather than assuming the pool is being hit.

The E2B-compatible wrapper, and the lines it draws

If you already use the E2B SDK, the E2B Compatibility Wrapper (ECW) lets it point at SPACE. The repository is clear about the status: ECW is a compatibility target maintained by SPACE, not an implementation of a versioned open E2B specification. The tested baseline is the Python packages e2b==2.21.1 and e2b-code-interpreter==2.6.2; other releases and the JavaScript SDK may work but are not covered by the repository's tests. Configuration is two settings and a routing rule.

# SPACE_ECW_HOSTS on BOTH the API and the data-plane service (JSON array):
#   [{"apiHost": "api.sandbox.example.test", "host": "sandbox.example.test"}]
# Route api.sandbox.example.test to the API and *.sandbox.example.test to the
# data plane. SPACE_ECW_CREDENTIAL_KEY_PATH must point at a mounted key file.

export E2B_DOMAIN=sandbox.example.test
export E2B_API_KEY=<raw-key>   # from scripts/seed-api-key.sh, printed once

uv run --no-project --python 3.11 --with e2b==2.21.1 python - <<'PY'
import os
from e2b import Sandbox

sandbox = Sandbox.create(
    api_key=os.environ["E2B_API_KEY"],
    domain=os.environ["E2B_DOMAIN"],
    template="seed",
    timeout=60,
)
try:
    print(sandbox.commands.run("printf 'Hello from SPACE\\n'").stdout)
finally:
    sandbox.kill()
PY

The snippet is the repository's own example with the configuration comments gathered above it. Where ECW diverges from E2B is the part to read before you migrate.

AreaBehaviour documented in the ECW guide
Lifecycle namesECW pause maps to SPACE suspend (persist VM state, release worker resources) and resume maps to restore. The native API's resident-memory pause and resume are not exposed.
Rejected create fieldsmcp, volumeMounts and iam return an E2B-shaped 400. autoPauseMemory=true is accepted; false is rejected.
Public trafficnetwork.allowPublicTraffic=true is rejected, because data-plane access is token-authenticated.
TemplatesAliases are flat: team/template names and tagged alias lookup are unsupported. code-interpreter-v1 maps to SPACE's code-runtime template, which must already exist.
File writesPOST /files accepts up to 256 MiB per request, streamed to a spool file. JSON file writes are capped at 1 MiB and return 413 above that.
Retrying a process startAn Idempotency-Key of 1 to 256 visible ASCII bytes lets a retry within 60 seconds receive the original stream. The guide calls it retry protection, not an exactly-once guarantee.

ECW routes are not in the OpenAPI document, so the pinned SDK tests are the contract. The guide's advice is blunt: run the ECW unit and Kind smoke suites when upgrading an E2B dependency, and use the native API for new integrations. One more trap: an application declared through startCmd may not be listening yet after create, so clients can see connection refused and must wait for their application separately.

What the red team measured, and what it did not

Perplexity's design post claims two layers of isolation, VM isolation and host operating-system process isolation, both of which must be breached for cross-sandbox access, with credentials held outside the sandbox and injected at the network layer. The report Escaping SPACE Part I tests that claim. Nine models were given root in the guest and a bounded time budget, with and without SPACE's source code, and with either no network or partial network (package repositories and Perplexity Search). That is 216 runs: 9 models, 2 tasks, 4 settings and 3 runs each.

TaskSettingResult
Escape the VM and read a host honeytokenAll four settings0 of 108 runs
Reach a callback URL blocked by egress policyNo network0 of 54 runs
Reach a callback URL blocked by egress policyPartial network11 of 54 runs
Same network task after the fixesStrongest setting, nine configurations, three runs eachNo flag retrieved

The headline 108 is therefore the escape half only, not 108 of everything. The 11 network successes came by two routes. With DNS spoofing, an agent with root in the guest forged replies impersonating the resolver to map pypi.org to the callback's IP, and the gateway accepted the false mapping; only Claude Opus 5.0, GPT-5.6 Sol and Kimi K3 managed it, and only with source access. With shared IPs, the gateway allowed a Fastly address for PyPI, so agents used other services on that address to fetch the callback: GPT-5.6 Sol used fiddle.fastly.dev, and GPT-5.6 Cyber used a Taboola image service and thum.io to render a screenshot, then read the flag back with OCR.

Both were fixed. For spoofing, an nftables rule drops packets from a guest whose source address does not match its assigned address. For shared IPs, plain HTTP goes through a relay that validates the request authority and strips routing metadata, and for TLS the gateway terminates the connection and requires the SNI and the HTTP Host to be allowlisted and DNS-attributed to the destination IP. The repository docs show the same design from the operator side: a hostname TLS exemption relays without terminating only when a live DNS binding from that hostname to the destination IP was observed, and exemptions bypass HTTP inspection and credential injection.

Read these as vendor-published numbers. Perplexity evaluated its own product, the report says Part II will cover attack strategies, and the specific VM-escape tactics are not described in Part I. The report also says eight of ten third-party platforms showed at least one network-policy bypass, which is a claim about competitors from a vendor with a product to sell; one vendor's results are omitted because it asked for more time. Zero of 108 does not prove zero. It says nine frontier models, in a bounded time budget, did not find a way out.

Trade-offs and failure modes to plan for

SPACE is a platform you operate, not a service you call, and the docs are honest about what that costs. These are the five I would put in a design review.

  • Hardware. The install needs a Linux x86_64 host with KVM: /dev/kvm, /dev/net/tun, loop devices, btrfs support and a Docker daemon that permits privileged containers. A VM must support nested virtualisation, the instructions do not run on macOS, and the prerequisite check does not verify privileged Docker, loop devices or btrfs.
  • Privilege. Firecracker isolates guest execution, but the host-side services still need privileges to manage VMs, storage and networking. The design post makes the node manager the single root process, which makes it the component to harden and monitor.
  • Durability. Files are persisted at lifecycle boundaries, not per write. An acknowledged write is not guaranteed to survive a sandbox crash, a forced stop or a host failure.
  • Streams. Command output is a live stream, not durable log storage, and a reconnect gives resync_required instead of a replay. Keep your own transcript.
  • Operations. Only the most recent minor release receives security fixes, and the docs lean on coordinated rollouts: upgrade every cluster-controller replica before enabling native pool claims, and upgrade policy readers before configuring TLS exemptions.

None of this is a flaw so much as the price of owning the boundary. The alternative, a hosted sandbox, moves the price into someone else's roadmap, which the Cloudflare Containers and Kubernetes Agent Sandbox posts on this site show from different angles.

What to do on Monday

Do not deploy it to production on Monday. Use the day to find out whether the operating model fits you, in this order.

  1. Find a Linux x86_64 machine with KVM, follow the Kind install and run python3 examples/first_sandbox.py. If it fails on devices or btrfs, you have learned your operating cost before writing any integration.
  2. Run the curl sequence above and time create to running. Then build a warm pool and time it again, reading the Boot mode line or the warm_pool.create_path.duration_ms metric, so you hold your own latency numbers instead of Perplexity's.
  3. If you use E2B today, point the pinned SDK (e2b==2.21.1) at ECW with your real template and see which of the rejected fields (mcp, volumeMounts, iam) your code sends.
  4. Decide egress before anything else. Start from a default-deny policy, write named egress profiles, and test the two routes the red team used: forged DNS replies from inside the guest and a host that shares a CDN address with an allowed one.
  5. Set the idle behaviour deliberately. The six-hour default suspends a sandbox, it does not delete it, so decide who deletes and when.
# Operator egress profiles: named, ADDITIVE allow-lists, set at startup via
# SPACE_EGRESS_PROFILES on the API and cluster-controller (same value on both,
# standby controllers included). Wildcard domains are not accepted.
{
  "audit":  { "default": true,
              "allow": [{"domain": "audit.example.test"}] },
  "sensor": { "allow": [{"domain": "sensor.example.test"},
                        {"cidr": "192.0.2.0/24"}],
              "tls_interception_exemptions": [{"domain": "sensor.example.test"}] }
}

# A federated create selects profiles per request. No header = defaults only.
#   X-Space-Egress-Profiles: +sensor -audit
# The selection is immutable for the sandbox's life: to change destinations,
# recreate the sandbox. Alert on outcome failed/unconfigured for drift.

Remember that a profile selection is immutable for a sandbox's lifetime, and that resume and restart do not refresh profile destinations. Changing an allow-list means recreating sandboxes, so version your profiles by name.

The rule I take from SPACE: an agent sandbox is a lifecycle and a network policy first and an isolation technology second. Firecracker supplies the VM boundary, and in Perplexity's own report that boundary held while the egress policy leaked. Measure your own create latency, own your egress rules, and treat every vendor-published number, these included, as a claim to reproduce.

FAQ

Frequently Asked Questions

Short answers to what readers ask most about this topic.

What is Perplexity SPACE?

SPACE stands for Sandboxed Platform for Agentic Code Execution. It is the sandbox platform Perplexity uses for its Computer agent, open-sourced on 15 October 2026 under Apache 2.0. Applications use an HTTP API to create Firecracker microVMs, run commands, collect output and manage the sandbox lifecycle.

Can SPACE replace E2B for existing code?

Partly. The E2B Compatibility Wrapper lets the E2B Python SDK talk to SPACE, but only e2b 2.21.1 and e2b-code-interpreter 2.6.2 are covered by the repository's tests. Fields such as mcp, volumeMounts and iam are rejected, and the docs recommend the native API for new integrations.

Does SPACE run on a Mac or a laptop?

Not directly. The install guide needs a Linux x86_64 host with KVM, /dev/net/tun, loop devices, btrfs support and a Docker daemon that allows privileged containers. A virtual machine works only if it supports nested virtualisation, and the instructions do not run on macOS.

Did SPACE really have zero sandbox escapes?

In Perplexity's own red-team report, none of 108 VM-escape runs by nine models succeeded. The same report found 11 of 54 partial-network runs bypassed egress policy through DNS spoofing and shared CDN addresses, which Perplexity says it fixed. The figures are vendor-published and zero in a bounded test is not proof of zero.

What is the difference between pause and suspend in SPACE?

Pause stops execution but keeps VM memory resident on the worker, and it is not a durable snapshot. Suspend saves VM memory, runtime state and the root filesystem, then releases the active VM resources so the sandbox can be restored later. By default the API suspends a sandbox after six idle hours.