Answers you can trust, from Codeables
Every page on Codeables is structured and verified — built so people and the AI agents they rely on can trust it. Explore more from the source behind this answer.
Explore Codeableshosting options for running untrusted code with strong isolation (per-job sandboxes)
Most teams discover they’re running untrusted code the hard way—students pasting solutions, customers uploading plugins, or an AI agent happily emitting shell commands. At that point, “just run it in a container” stops sounding safe enough, and you start looking for hosting options that give you strong isolation and per‑job sandboxes without building a whole cloud yourself.
Quick Answer: The safest way to run untrusted code with strong isolation is to give every job its own short‑lived sandbox: either a microVM per job (Fly Sprites, Fly Machines, Firecracker‑based platforms), or a locked‑down container in a hardened multi‑tenant runtime. For most teams, per‑job VMs or microVMs in 2026 are the practical “don’t wake me at 3 a.m.” choice.
The Quick Overview
- What It Is: A set of hosting options and patterns for executing untrusted or user‑supplied code, where each job runs in a strongly isolated sandbox that can be created and destroyed on demand.
- Who It Is For: Teams building coding challenge platforms, AI agent backends, plugin execution systems, workflow engines, ETL pipelines, or any service that has to run code you didn’t write and can’t fully trust.
- Core Problem Solved: How to give each job its own safe environment—no data leakage, no noisy neighbors, no “oops, that user just rm -rf’d the host”—while still getting decent startup times and sane costs.
How It Works
At a high level, every “run untrusted code” architecture has three parts:
- A control plane that receives work (API call, queue message, webhook) and decides where/how to run it.
- An isolation runtime (VM, microVM, container, or sandbox) that executes that job in its own environment.
- A lifecycle manager that provisions, monitors, and tears down those sandboxes, ideally per job.
Fly.io’s take on this: everything runs on Fly Machines (hardware‑virtualized containers that launch fast and bill by the second). For stronger “this thing might be actively hostile” cases, Sprites give you hardware‑isolated sandboxes that start in under a second, each with its own private network and the option to snapshot and restore.
You can think of strong per‑job sandboxes as a spectrum:
- Full VMs per job – Maximum isolation, higher overhead.
- MicroVMs per job (e.g., Sprites) – Near‑VM isolation with “fast enough for HTTP” startup times.
- Containers with hardening – Cheaper, faster, but you’re betting more on your runtime’s security posture.
- Language VMs / in‑process sandboxes – Great for extra safety inside your app… but not enough for hostile code alone.
The hosting choice is about where on that spectrum you want to live.
The main patterns
- Ephemeral VM / microVM per job
- Spin up a tiny VM or microVM for each job, run the payload, collect output, kill it.
- Strong isolation (hardware boundary), clean state per run, easy to reason about.
- Warm sandbox pool
- Keep a pool of pre‑booted sandboxes and hand them jobs to avoid cold start spikes.
- Periodically recycle to avoid state bleed or resource leaks.
- Shared runtime with in‑process sandboxing
- Run jobs inside a long‑lived process using WASM,
seccomp, or language sandboxing. - Best used as a second layer of defense, not your only line.
- Run jobs inside a long‑lived process using WASM,
Below we’ll walk through concrete hosting options and how per‑job sandboxes look on each, with Fly Machines and Sprites as the reference model.
Option 1: Per‑Job VMs / MicroVMs (Fly Sprites & Machines)
If you want strong isolation with an actual hardware boundary but don’t want to be a full‑time virtualization engineer, this is where you start.
How it works on Fly.io
- Fly Machines are hardware‑virtualized containers that launch quickly and bill per second. They’re the right primitive if you want “serverless‑like” elasticity with VM semantics.
- Sprites are hardware‑isolated sandboxes on top: each Sprite is a self‑contained environment that:
- Starts in under a second.
- Runs in complete isolation (no shared environment, no state bleed between jobs).
- Comes with automatic private networking and end‑to‑end encryption.
- Can be checkpointed and restored so you can snapshot environments, then resume them later.
Pattern:
- Control plane receives job.
- It
POSTs to the Fly API (or usesflyctl) to start a Machine or Sprite, passing your job spec/env. - The sandbox runs the job, writes logs and output.
- Control plane collects result, then destroys or snapshots the sandbox.
Job 1 and Job 2 never share a filesystem, PID space, or network namespace. Nothing leaks. That’s the whole point.
Example: one Machine per job
High‑level flow:
# Build your sandbox image once
fly deploy --image ghcr.io/your-org/sandbox-runner:latest --remote-only
Your sandbox-runner entrypoint might:
- Read a job spec from env or a queue.
- Fetch user code.
- Run it inside a restricted sub‑environment (e.g., another
chrootor WASM). - Write results to stdout or object storage (Tigris).
Your control plane (could be a Fly app too) does:
curl -X POST "https://api.machines.dev/v1/apps/your-sandbox-app/machines" \
-H "Authorization: Bearer $FLY_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"region": "iad",
"config": {
"image": "ghcr.io/your-org/sandbox-runner:latest",
"env": {
"JOB_ID": "abc123"
},
"guest": { "cpus": 1, "memory_mb": 256 }
},
"auto_destroy": true
}'
- Machine starts near the user (
iadhere). - Job runs, writes logs.
auto_destroy: trueensures the Machine is torn down when it exits. No cleanup cron. No zombie sandboxes.
Sprites layer stronger sandboxing on top, with even more isolation and the ability to snapshot a pre‑warmed environment, then fork it for each job.
When this is the right choice
Use per‑job VMs or microVMs when:
- You don’t fully trust the code (malicious, AI‑generated, or from unknown users).
- Regulatory / compliance needs push you toward clean, auditable job boundaries.
- You want clean logs and metrics per job, independent of your main app.
- You don’t want noisy neighbors or cross‑tenant data leakage.
This is the pattern used by platforms like CodeCrafters: they spin up a Machine per submission, run the code in a completely isolated environment, and tear the Machine down after. They even cache Machines ahead of time so they always have warm capacity ready.
Option 2: Hardened Containers in Multi‑Tenant Runtimes
A lot of “run code” services rely on containers as the primary isolation mechanism: Docker, containerd, Kubernetes pods, etc. If you go this route, you want to layer several protections:
- Non‑root containers with user namespaces.
seccomp, AppArmor/SELinux profiles.- Read‑only root filesystem where possible.
- Tight resource limits (CPU, memory, PIDs, disk, network).
Typical hosting setups
- Managed Kubernetes with a strong sandbox runtime (e.g., gVisor, Kata).
- DIY “job runner” on a VM that spins one container per job.
- FaaS platforms that expose container runtimes under the hood.
Pattern:
- Control plane schedules a pod/job/container for each incoming request.
- Job runs with a strict security profile.
- Pod is torn down; runtime cleans up.
Benefits:
- Startup speed: Often hundreds of milliseconds to a few seconds.
- Density: You can pack many jobs onto a single node.
- Familiar tooling: Docker images, Kubernetes manifests.
Limitations:
- Kernel is shared; you’re relying heavily on the container runtime’s correctness.
- Breakout vulnerabilities do happen. You have to track CVEs and update often.
- Per‑job cleanup must be reliable, or you end up with state bleed or noisy neighbors.
This option is acceptable if you’re running code from semi‑trusted sources, or you’re adding another layer of isolation (e.g., WASM) inside the container. For “students from the internet running arbitrary C code with fork() and syscalls,” it’s on the riskier side unless you’re very disciplined.
Option 3: Language & In‑Process Sandboxes (WASM, V8, etc.)
This is the “optimizer” layer, not the first line of defense. Think:
- WebAssembly runtimes (Wasmtime, Wasmer, WasmEdge).
- JavaScript engines (V8 isolates, Deno, Node workers).
- Language VMs (Lua, Python
restrictedmodes, etc.).
Pattern:
- Your host process runs in a VM/Machine/container.
- Each job runs in an in‑process sandbox with restricted imports and syscalls.
- You kill the sandbox or the whole process if it misbehaves.
Benefits:
- Very fast startup: Milliseconds to spin up an isolate/instance.
- Fine‑grained control: You decide what APIs (filesystem, network, clock) are exposed.
- Great for multi‑tenant logic: Thousands of jobs can share one host process.
Limitations:
- If the host process is compromised, it has the same privileges as the host container/VM.
- Bugs in the sandbox runtime are now security bugs.
- You still want an outer isolation layer (like a Machine/Sprite) if the code is hostile.
The production‑hardened pattern is: WASM or JS isolates inside a per‑job VM/microVM. That way, each job is sandboxed twice, and a single breakout doesn’t immediately land the attacker on the bare metal.
Features & Benefits Breakdown
Here’s how a strong per‑job sandbox model looks when built on top of Fly Machines & Sprites.
| Core Feature | What It Does | Primary Benefit |
|---|---|---|
| Hardware‑isolated per‑job sandboxes (Sprites) | Spins up a Sprite for each job with its own kernel boundary, filesystem, and private network. | Strong isolation: untrusted code can’t escape or see other tenants, and each job starts from a clean slate. |
| Fast, pay‑by‑the‑second compute (Machines) | Launches hardware‑virtualized containers fast enough to handle HTTP, billing CPU and memory down to the second. | Makes “one VM per job” financially and operationally reasonable, even at high volume. |
| Snapshot & restore environments | Checkpoints a sandbox state and restores it later for new jobs. | Warm, reproducible environments without re‑provisioning on every run—better latency and less thrash. |
Ideal Use Cases
-
Best for coding challenge platforms & education portals:
Because they must run arbitrary student code in many languages, safely, with clean logs per submission and without one student crashing everyone else’s environment. -
Best for AI agent backends & plugin execution systems:
Because they constantly execute AI‑generated scripts, tools, or user‑supplied plugins that might go off the rails. Per‑job sandboxes keep those experiments away from your core infrastructure and other customers.
Other good fits:
- CI pipelines that need isolated build steps per customer.
- Data processing pipelines where user transformations are arbitrary code.
- Multi‑tenant SaaS platforms allowing “bring your own code” customization.
Limitations & Considerations
-
Startup latency overhead:
Even fast Machines and Sprites have a startup cost. For extremely latency‑sensitive workloads (sub‑50ms), you’ll likely run a warm pool of sandboxes and reuse them, with periodic recycling. The pattern CodeCrafters uses—caching Machines ahead of time and assigning them to users—is a good reference. -
State and storage management:
Per‑job sandboxes are ephemeral by design. If you need persistent state:- Write to object storage (e.g., Tigris) or a database (e.g., Fly Postgres) from inside the job.
- Treat the sandbox filesystem as scratch space only.
- If you need a pre‑seeded environment (datasets, compilers), build it into the image or snapshot it and restore it for jobs.
Pricing & Plans (Conceptual)
Concrete numbers vary by provider, but for per‑job sandboxes you’ll usually see:
-
Compute‑based pricing:
Billed per CPU‑second and GB‑second of memory, sometimes with a minimum time per allocation. -
Storage & network:
Object storage, snapshots, and egress are billed separately.
On Fly.io, the mental model is:
- Small Machines / Sprites: Best for high‑volume job runners where each job is short‑lived and you care about per‑second billing. You pay only for the time each Machine is actually running.
- Larger, long‑lived Machines: Best for control planes, job queues, and orchestration services that manage the lifecycle of those sandboxes.
Design for spikiness: untrusted code workloads are often bursty (lab deadlines, AI batch jobs, nightly imports). You want a platform that scales up into thousands of Machines and right back down, without paying for idle capacity.
Frequently Asked Questions
Is a container enough isolation for running untrusted code?
Short Answer: Not by itself if the code is truly untrusted. Use a VM/microVM boundary or an additional sandbox layer.
Details: Containers share the host kernel; that’s their whole efficiency trick. For internal tools, that’s fine. For “run arbitrary user code off the internet,” you’re betting a lot on your runtime and kernel configuration. The safer pattern is:
- Run each job in a Machine or Sprite (VM boundary).
- Inside that, optionally run the code in a WASM or language sandbox.
- Add resource limits (
ulimit, cgroups) and a read‑only root filesystem.
That way, a container runtime bug or language sandbox escape doesn’t immediately give an attacker access to your physical host or to other tenants.
How do I prevent jobs from leaking data between each other?
Short Answer: Don’t reuse sandboxes for untrusted tenants; give each job its own environment and tear it down or snapshot/reset it between runs.
Details: Data leakage between jobs usually comes from:
- Shared filesystems or volumes.
- Long‑lived processes with cached state.
- Databases without per‑tenant isolation.
Per‑job sandboxes help because:
- Each Machine/Sprite has its own filesystem; when it dies, that state goes with it unless you explicitly write out to storage.
- Private networking per sandbox means no accidental cross‑job network access by default.
- You decide exactly what external resources (databases, buckets) the sandbox can talk to, using network policy and credentials.
If you do reuse sandboxes for latency reasons, implement a strict reset protocol: wipe temp dirs, clear any caches, drop connections, and periodically destroy and recreate the sandbox to bound how long any state can live.
Summary
Running untrusted code safely is mostly about drawing hard lines: this job runs here, with these limits, for this long, and nothing it does can escape or contaminate anything else. Full VMs used to be too heavy for “one per job,” but hardware‑virtualized containers and microVMs changed that. Platforms like Fly.io give you Machines and Sprites that start fast enough to sit behind an HTTP request, so you can treat VMs like processes and give every job its own sandbox.
If your threat model is “someone on the internet might try to break out of this,” reach for per‑job Machines or Sprites and optionally layer WASM or language sandboxes inside. You get clean logs, predictable performance, and the ability to scale into thousands of isolated jobs without inventing your own cloud platform.