Scale Interview
System DesignHard

Design a Remote IDE

Replit / GitHub Codespaces System Design

Overview

A remote IDE lets a user open a code editor in their browser and get a full development environment in the cloud — a filesystem, a terminal, a compiler, and the ability to run their code — without installing anything locally. Replit, GitHub Codespaces, and Gitpod all work this way. You click "open," and a few seconds later you have a Linux box that is yours: you edit files, run a build, start a web server, and see the output, all from a browser tab.

It is labeled "Hard" because of one property that sets it apart from almost every other system design: you are running untrusted code that strangers wrote, on your machines, on purpose. A normal web service only runs code you wrote. Here, the whole product is executing arbitrary programs supplied by thousands of different users — programs that might try to escape their sandbox, mine cryptocurrency, attack other tenants, or simply use every CPU cycle on the box. So the headline problem is isolation: thousands of multi-tenant users, each with their own walled-off workspace, none able to harm another or the host.

The other hard parts follow from that: provisioning a sandboxed environment fast (nobody waits 30 seconds to start coding), keeping the filesystem alive across disconnects even though the compute itself is throwaway, and doing all of this without keeping millions of idle environments running and burning money.

In this walkthrough you'll scope it, size it, model the data, design the API, draw the open / run / persist flows, and examine the three things that make or break the design: isolation of untrusted code, the workspace lifecycle and cold start, and persistence.

Out of scope (state it): the editor UI internals (how the text buffer, syntax highlighting, and cursor work in the browser), language servers in depth (we mention where they run), and real-time collaborative editing — two people typing in the same file at once. That last one is a separate problem (operational transforms or CRDTs over a live document, like the chat / docs co-editing problem) and we flag it as an extension, not core scope.

Functional requirements

  • Open a workspace. A user clicks a project and gets a ready-to-use environment — a filesystem with their code, a terminal, and the tools to build and run it.
  • Edit files. The browser reads and writes files in the workspace, with low latency, as if they were local.
  • Run code and use a terminal. The user can execute arbitrary programs, start a long-running server, and see stdout/stderr streamed back to the browser in real time.
  • Persist work. Files (and ideally running processes) survive a disconnect, a browser refresh, and the workspace being suspended to save money — the user comes back and their work is still there.
  • Isolation between users. One user's code and resource usage must never read, corrupt, or starve another user's workspace or the host.

Tip

Lead with these functional requirements, then make isolation the headline non-functional requirement. If you only describe "an editor in the browser" and skip "we are running untrusted code," you have missed the entire point of the question — the interviewer is probing how you sandbox arbitrary programs safely.

Non-functional requirements

  • Strong isolation (security) — the headline. Each workspace runs code the platform did not write and does not trust. A user must not be able to break out of their sandbox onto the host, read another tenant's files, or send traffic to other tenants. This single requirement drives the whole compute design (deep dive 1).
  • Resource fairness. One workspace running an infinite loop or allocating all the memory must not slow down or crash the workspaces sharing the same host. Every workspace gets bounded CPU, memory, disk, and network (deep dive 1).
  • Fast cold start. Opening a workspace should feel near-instant — target a few seconds, not tens of seconds. The fresh-environment provisioning time is the number the user feels most (deep dive 2).
  • Durable persistence. The filesystem must survive disconnect, suspend, and host failure. Compute is cheap and replaceable; the user's files are not (deep dive 3).
  • Cost efficiency at scale. Most workspaces are idle most of the time. You cannot afford to keep millions of idle environments holding CPU and memory, so idle workspaces must be suspended and their compute reclaimed (deep dive 2).
  • Horizontal scale. Workspaces, hosts, and the routing layer all grow with users; every layer must scale by adding machines.

Estimations

State assumptions; the goal is to justify suspending idle workspaces, the warm-pool pattern, and separating storage from compute — not to be exact.

Rounded assumptions

  • Registered users: ~10M, with ~1M active on any given day.
  • Concurrently open (actively connected) workspaces at peak: ~100K.
  • Total workspaces that exist (each user has saved projects): ~50M — but the vast majority are idle at any moment.
  • Per active workspace: ~1 vCPU and ~1–2 GB RAM as a baseline (more for heavy builds).
  • Cold start target: ~2–5 seconds from click to usable terminal.
  • Per-workspace disk: ~1–5 GB of files (source, dependencies, build artifacts).

The cost reality (this drives the whole lifecycle design). If 100K workspaces are open at peak, that's ~100K vCPUs and (at ~1.5 GB each, the midpoint of the 1–2 GB range) ~150 TB of RAM in use — already a large, expensive fleet. But there are 50M workspaces in total. If you tried to keep even 1% of them (500K) "warm" and running, you'd need 500K vCPUs sitting idle, mostly doing nothing. At cloud prices that is millions of dollars a month to keep environments running that nobody is currently typing into. You cannot keep idle workspaces running. They must be suspended — their compute returned to the pool — while their files are kept safe in cheap storage.

How the numbers affect the design

Signal from the numbers Design decision
50M workspaces but only ~100K open at once Suspend idle workspaces; only pay compute for active ones.
Compute is the expensive part, idle 99% of the time Separate durable storage from ephemeral compute, so a suspended workspace costs only disk.
Cold start must feel instant (~2–5 s) Keep a warm pool of pre-booted sandboxes to skip boot time.
Each workspace runs untrusted code Strong per-workspace isolation (microVM / gVisor) + cgroup resource limits.
One tenant must not starve another Hard CPU / memory / disk-I/O limits per workspace via cgroups; disk capacity via a sized volume / filesystem quota.
Browser must reach the one host holding a workspace A routing / session layer mapping workspace → host.

The two big levers fall straight out of the numbers. Suspend-on-idle is forced by the 50M-vs-100K gap — you can't run them all. And once compute is throwaway, storage must be separated from it so the files survive the suspend. Those two ideas shape deep dives 2 and 3; isolation (deep dive 1) is forced by "untrusted code" independent of scale.

Core entities / data model

The model separates three things: the account/project bookkeeping (small, strongly consistent), the workspace runtime state (where it lives, is it running), and the files (large, durable, separate from compute).

Entity Key fields
User user_id (PK), email, plan, quota (max workspaces, max resources)
Workspace workspace_id (PK), user_id, name, image_ref (base environment), volume_id, status (running/suspended/stopped), resource limits
Host host_id (PK), region, capacity, free CPU/mem, current workspace count
Placement (workspace_id)host_id, endpoint, started_at — where a running workspace lives
Volume volume_id (PK), workspace_id, storage backend, size, last_snapshot_id
Snapshot snapshot_id (PK), volume_id, created_at, the durable point-in-time copy of the filesystem

The key separation is Workspace (the durable record that always exists) vs Placement (which only exists while the workspace is running on some host). When a workspace is suspended, its Placement row is deleted — there is no host assigned — but the Workspace and its Volume persist. Reopening it creates a new Placement on a (possibly different) host, then restores the volume.

The Volume is the durable home of the filesystem, deliberately stored separately from whatever container or VM is currently running the workspace. The compute is ephemeral and content-free at rest; the volume is the thing that must never be lost (deep dive 3).

Important

Keep the durable record (Workspace, Volume, Snapshot — these survive forever) separate from the runtime placement (Placement, Host — these exist only while running). A common mistake is to model a workspace as "a container," conflating the long-lived identity with the short-lived process. The container is disposable; the workspace identity and its files are not.

API design

The contract has three parts: lifecycle control (open/suspend), file access, and the real-time channel that streams a terminal and process output to the browser. Label the request Body: and the response Returns:.

Open (or resume) a workspace

POST /api/workspaces/{workspace_id}:open
Body: (empty — identity comes from the session)
200 OK
Returns: { "status": "running", "endpoint": "wss://ws-7f3a.ide.example.com/connect",
           "cold_start_ms": 2400 }

This is the heavy call. The control plane picks a host (ideally a pre-booted one from the warm pool), restores the workspace's volume onto it, starts the sandbox, and returns the endpoint — the specific host the browser must connect to for this workspace. If the workspace was already running, it returns the existing endpoint immediately. The browser then opens a persistent connection to that endpoint (below).

Suspend a workspace

POST /api/workspaces/{workspace_id}:suspend
Body: (empty)
200 OK
Returns: { "status": "suspended", "snapshot_id": "snap_91c2" }

Snapshot the filesystem to durable storage, then tear down the sandbox and return its compute to the pool. Called explicitly, or automatically by an idle timer (deep dive 2). After this, the workspace costs only storage, not compute.

Connect — the real-time channel (per-workspace, persistent connection)

Browser opens a WebSocket to the workspace's endpoint:  wss://ws-7f3a.ide.example.com/connect
  → authenticates, then keeps the connection OPEN to that one host

Browser → workspace events:
  { "type": "exec",      "cmd": "npm test" }        (run a command in the sandbox)
  { "type": "stdin",     "data": "y\n" }            (send keystrokes to a running process)
  { "type": "fs.write",  "path": "/app/main.py", "data": "..." }
  { "type": "fs.read",   "path": "/app/main.py" }

Workspace → browser events (pushed, no request):
  { "type": "stdout",    "data": "..." }            (process output, streamed live)
  { "type": "stderr",    "data": "..." }
  { "type": "exit",      "code": 0 }
  { "type": "fs.changed","path": "/app/main.py" }   (file changed on disk, e.g. by a build)

Editing and running cannot be plain request/response: the terminal streams output continuously and a running server prints logs whenever it wants, so the browser must be pushed to. The browser opens one WebSocket to the specific host running its workspace and shuttles file edits, terminal keystrokes, and exec commands over it; the host streams stdout/stderr back live. This channel is stateful and host-specific — unlike a stateless REST API, it only works against the one host holding this workspace (deep dive 2 covers how the browser is routed there).

List workspaces

GET /api/workspaces
Returns: 200 { "workspaces": [ { "workspace_id": "...", "name": "...", "status": "suspended" }, ... ] }

Load the user's project list — what the dashboard shows on launch, with each workspace's current status.

High-level architecture

There is a clean split between a control plane (decides where a workspace runs, manages lifecycle and routing — small, strongly consistent) and a data plane (the hosts that actually run the sandboxes and stream I/O — large, where the untrusted code lives). Three flows matter: opening / provisioning a workspace, edit + run with the browser connected, and suspend / snapshot for persistence. We'll walk each, then combine.

1. Open / provision a workspace

The control plane finds a host, restores the volume, and starts a sandbox — ideally reusing a pre-booted one to keep cold start low.

flowchart LR
    Browser[Browser] -->|"1. open workspace"| CP[Control Plane]
    CP -->|"2. pick a host (warm pool)"| Pool[Warm Pool / Scheduler]
    Pool -->|"3. assign host + sandbox"| Host[Workspace Host]
    CP -->|"4. restore volume from snapshot"| Store[(Durable Storage)]
    Store -->|"5. filesystem mounted"| Host
    CP -->|"6. return endpoint for THIS host"| Browser
    Browser -.->|"7. open WebSocket to that host"| Host
  1. The browser asks the control plane to open the workspace.
  2. The control plane (scheduler) picks a host with free capacity — preferably one with a pre-booted sandbox already waiting in the warm pool, so there's no boot delay.
  3. The host claims a sandbox for this workspace.
  4. The control plane tells the host to restore the workspace's volume — the durable filesystem — from its latest snapshot in durable storage.
  5. The filesystem is mounted into the sandbox; now the environment has the user's actual files.
  6. The control plane records the Placement and returns the endpoint — the address of this specific host.
  7. The browser opens its persistent connection directly to that host. From here on, edits and commands flow over that channel.

Caution

Never start a workspace and run user code before the right filesystem is mounted and the sandbox limits are in place. Starting an unconstrained container "to save a second" and tightening it later leaves a window where untrusted code runs without resource limits or full isolation — exactly when an attacker wants to act.

2. Edit + run (browser connected to the workspace)

Once connected, the browser talks to the one host holding the workspace; the host runs commands inside the sandbox and streams output back.

flowchart LR
    Browser[Browser] -->|"1. exec / fs.write (WebSocket)"| Host[Workspace Host]
    Host -->|"2. run inside the sandbox"| Sandbox[Isolated Sandbox]
    Sandbox -->|"3. read/write files"| Vol[(Mounted Volume)]
    Sandbox -->|"4. stdout / stderr stream"| Host
    Host -->|"5. push output (WebSocket)"| Browser
  1. The user types a command or saves a file; the browser sends it over the WebSocket to the host.
  2. The host executes it inside the isolated sandbox — the untrusted code only ever runs in the sandbox, never directly on the host.
  3. The program reads and writes files on the mounted volume.
  4. stdout/stderr from the program stream back to the host process that manages this connection.
  5. The host pushes that output down the WebSocket; the browser shows it in the terminal in real time.

Note

The host process that holds the WebSocket is a thin "agent" that lives outside the sandbox but talks into it. It relays I/O and enforces limits. The untrusted program never has direct access to the network socket or the host — it only ever sees a pipe to its own stdin/stdout. Keeping the agent outside the blast radius is what lets you trust the I/O path even though the workload is untrusted.

3. Suspend / snapshot (persistence)

When the workspace goes idle, snapshot its filesystem to durable storage and free the compute.

flowchart LR
    Timer[Idle Timer / explicit suspend] -->|"1. suspend workspace"| Host[Workspace Host]
    Host -->|"2. flush + snapshot filesystem"| Snap[Snapshot]
    Snap -->|"3. write snapshot"| Store[(Durable Storage)]
    Host -->|"4. tear down sandbox, free CPU/mem"| Pool[Warm Pool / Capacity]
    Host -->|"5. delete placement"| CP[Control Plane]
  1. An idle timer fires (or the user explicitly suspends).
  2. The host flushes pending writes and takes a point-in-time snapshot of the filesystem.
  3. The snapshot is written to durable storage (a block-volume snapshot or an object-store copy).
  4. The sandbox is torn down and its CPU/memory return to the pool for other workspaces.
  5. The control plane deletes the Placement — the workspace now has no host and costs only storage. Reopening runs flow 1 again, restoring this snapshot.

4. Combined architecture

Putting it together — the control plane schedules and routes; the hosts run sandboxes; durable storage holds the volumes; the warm pool keeps cold start low.

flowchart LR
    Browser[Browser] -->|"open / suspend (control)"| CP[Control Plane]
    CP <-->|"schedule, place, route"| Pool[Warm Pool / Scheduler]
    CP -->|"where is this workspace?"| Reg[(Routing Registry: workspace -> host)]
    CP --> Meta[(Metadata DB: workspaces, volumes)]
    Browser -.->|"WebSocket to the right host"| Host[Workspace Host]
    Host -->|"runs untrusted code"| Sandbox[Isolated Sandbox + cgroup limits]
    Sandbox --> Vol[(Mounted Volume)]
    Host <-->|"snapshot / restore"| Store[(Durable Storage)]
    Pool -.->|"pre-booted sandboxes"| Host

The control plane owns the small, strongly-consistent state (which workspaces exist, where running ones are placed, scheduling) and the routing registry that maps workspace → host so the browser's WebSocket reaches the one host holding it (the same routing idea as a chat app's user → connection server map). The hosts run the isolated sandboxes with per-workspace resource limits, mount volumes, and stream I/O. Durable storage holds the volumes and snapshots, kept separate from compute so suspend/resume is cheap. The warm pool pre-boots sandboxes so opening a workspace skips the slow boot.

Deep dives

1. Isolation and running untrusted code

This is the headline. Every workspace runs arbitrary programs the platform did not write and cannot trust. The threat is concrete: a malicious user runs code that tries to escape the sandbox onto the host, where it could read other tenants' files, steal credentials, or take over the machine. Isolation answers two questions: how strong is the security boundary around each workspace, and how do you stop one tenant from starving the others.

The security boundary. The core decision is what separates a tenant's code from the host and from other tenants. There is a ladder of strength, and it trades security against startup speed and density (how many workspaces you pack per host).

Plain shared container (namespaces + a shared kernel), no extra sandbox (avoid)

Run each workspace as an ordinary container — Linux namespaces give it its own process/filesystem/network view, and that's the only boundary. The container shares the host kernel directly with every other tenant.

Worked example — what goes wrong:

flowchart LR
    Code["Tenant A's code finds a<br/>Linux kernel vulnerability<br/>(found regularly)"] -->|"exploits a syscall the<br/>container can still make"| Escape["Breaks out of the namespace<br/>onto the host kernel"]
    Escape -->|"one shared kernel =<br/>one exploit from full compromise"| Compromise["Reads Tenant B's files,<br/>host secrets, other containers"]
    style Code fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
  • Pro: lightest weight, fastest start, highest density — this is why plain containers are great for your own trusted services.
  • Con: a container is not a strong security boundary against hostile code. The kernel is a huge shared attack surface; a single kernel exploit breaks the whole isolation. Untrusted multi-tenant code is exactly the case containers were not designed to isolate.
  • Verdict: avoid as the only boundary for untrusted code. Fine for trusted internal workloads; dangerous when strangers supply the program.
Container + syscall-filtering sandbox (gVisor) (works, with caveats)

Keep the container model but put a sandbox kernel between the tenant and the real host kernel. gVisor intercepts the container's system calls in user space and services most of them itself, so the tenant's code rarely touches the real host kernel directly. This shrinks the attack surface dramatically.

Worked example:

flowchart LR
    Code["Tenant code makes a syscall<br/>(e.g. open a file)"] -->|"intercepted in user space"| GVisor["gVisor's user-space kernel<br/>handles it, NOT the host kernel"]
    GVisor -->|"kernel-exploit path mostly cut off"| Kernel["Only a small, hardened set<br/>of operations reach the real kernel"]
    style GVisor fill:#fef9c3,stroke:#ca8a04,color:#713f12
  • Pro: much stronger than a plain container, with far less overhead than a full VM. Starts fast, packs densely. A good middle of the ladder.
  • Con: adds some syscall overhead (I/O-heavy or syscall-heavy workloads run slower), and a few workloads hit unsupported syscalls. The sandbox kernel itself is now the thing that must be bug-free — a smaller surface, but not zero (gVisor's own sandbox kernel has had escape CVEs). And because its user-space kernel still forwards some syscalls to the host kernel, it reduces but does not eliminate the shared-kernel attack surface — a genuinely weaker boundary than a hypervisor, not near-equal to a microVM.
  • Verdict: works, with caveats — a solid choice when you want container-like speed but stronger isolation than namespaces alone. Many platforms run this for general workloads.
microVM per workspace (Firecracker) (recommended for untrusted code)

Give each workspace its own tiny virtual machine with its own kernel, on top of a hardware-virtualization boundary. A microVM like Firecracker boots in roughly 100 ms and uses little memory, so you get a real VM's isolation without a traditional VM's slow boot and heavy footprint. That ~100 ms is the microVM and guest-kernel boot itself; the cold start the user actually feels also includes starting the userspace environment and restoring the volume, so a workspace does not open in 100 ms (see deep dive 2). The hardware virtualization layer is a much stronger boundary than a shared kernel: escaping it means defeating the hypervisor, not just the kernel. To be precise, the host still exposes a (deliberately minimal) VMM / KVM / virtio device-emulation surface, and that device-emulation surface is the real thing an escape targets — Firecracker's stripped-down device model is precisely the mitigation that keeps it small.

Worked example:

flowchart LR
    VM["Each workspace = its own microVM,<br/>its own guest kernel"] -->|"Tenant A exploits a kernel bug"| Guest["Only compromises A's<br/>OWN guest kernel"]
    Guest -->|"still trapped by the<br/>hypervisor boundary"| Trapped["Cannot reach host or Tenant B<br/>without ALSO breaking the hypervisor"]
    Trapped -.->|"two boundaries: guest kernel + hypervisor<br/>instead of one shared kernel"| VM
    style VM fill:#dcfce7,stroke:#16a34a,color:#14532d
  • Pro: the strongest practical boundary for untrusted multi-tenant code — a per-tenant kernel plus hardware virtualization. Firecracker keeps boot and memory cost low enough to run many per host. This is what AWS Lambda and several IDE platforms use precisely because they run other people's code.
  • Con: still heavier than a bare container (more memory per workspace, slightly slower cold start than a warm container), and you manage a VM layer. Density is lower than plain containers.
  • Verdict: the recommended default when the workload is genuinely untrusted, which it always is here. The isolation strength is worth the modest density cost. Pair with gVisor for general workloads and microVMs for the hostile tail if you want both.

Caution

Running untrusted user code in a plain shared-kernel container with no extra sandbox is the single most dangerous mistake in this design. A container alone is a packaging boundary, not a security boundary against hostile code. State explicitly that you need a stronger sandbox — gVisor or a microVM — and name container escape as the threat you are defending against.

The mitigation ladder, summarized:

Boundary What separates tenant from host Strength Cost
Plain container (namespaces) Shared host kernel Weak vs hostile code Cheapest, fastest
gVisor User-space syscall-filtering kernel Strong Low overhead
microVM (Firecracker) Own guest kernel + hypervisor Strongest practical Moderate
Full VM Own kernel + hypervisor (heavyweight) Strongest Slow boot, heavy

Resource limits — stopping one tenant starving the rest. A strong security boundary stops escape; it does nothing about a workspace that simply uses every CPU cycle or all the memory on its host. That's where cgroups come in: every workspace is pinned to a hard quota — a CPU share, a memory ceiling, and a disk-I/O (throughput/IOPS) cap. Note that cgroups bound disk I/O, not disk capacity: how much the filesystem can hold is bounded separately, by a sized block volume or an overlay/project filesystem quota. Cap the process/thread count too — the pids cgroup controller (backed by ulimits) — to stop a fork bomb, the canonical untrusted-code DoS that memory and CPU ceilings don't directly prevent: a process that recursively spawns children exhausts the kernel's process table long before it hits any memory or CPU cap. If a workspace's code allocates past its memory ceiling, the kernel kills that workspace's processes, not its neighbors'. If it spins an infinite loop, it only burns its own CPU share. The microVM/container gets a fixed slice and cannot exceed it, so a "noisy neighbor" or a crypto-miner is contained to its own quota.

Warning

Resource limits are not optional polish — they are part of isolation. Without a memory ceiling, one workspace's runaway allocation triggers the host's out-of-memory killer, which can kill other tenants' processes. Without a CPU cap, one busy loop degrades every workspace on the host. cgroup quotas per workspace are as essential as the security boundary.

Network egress restrictions. Untrusted code with open internet access is a liability — it can attack other hosts, exfiltrate data, send spam, or DDoS a victim. Restrict each workspace's outbound network: run the workspace in its own network namespace with no route to the cloud metadata IP (169.254.169.254, the service that hands out host credentials — a classic target), and send all egress through a filtering proxy/NAT rather than letting the sandbox talk to the network directly. Rate-limit egress, and optionally allowlist only what's needed (package registries, git). The workspace should never be able to reach the control plane, the host's management interface, or another tenant's network.

2. Workspace lifecycle and cold start

Two forces pull against each other: opening a workspace must feel instant (a few seconds), but you cannot keep idle workspaces running because of the cost reality from the estimations (50M workspaces, only ~100K open at once). The lifecycle design reconciles them.

The lifecycle states:

flowchart TB
    Suspended["stopped / suspended<br/>no compute, files safe in durable storage<br/>(costs only disk)"] -->|"open"| Starting["starting<br/>host assigned, volume restoring,<br/>sandbox booting"]
    Starting --> Running["running<br/>connected, executing code<br/>(costs CPU + memory)"]
    Running -->|"idle timeout (e.g. no activity for N minutes)"| Suspending["suspending<br/>snapshot filesystem,<br/>tear down sandbox"]
    Suspending -->|"back to suspended"| Suspended
    style Suspended fill:#e0f2fe,stroke:#0284c7,color:#0c4a6e

The cold-start problem. Provisioning a fresh sandbox from scratch involves several slow steps: pick a host, boot a sandbox (a microVM or container), pull the base image, mount and restore the volume, and start the user's environment. Done naively on every open, that's tens of seconds — unacceptable. Two strategies:

On-demand provisioning — boot everything fresh on every open (avoid as the default)

When the user clicks open, then allocate a host, boot a fresh sandbox, pull the base image, and restore the volume — all in the critical path the user is waiting on.

Worked example:

 click open → schedule host (200ms) → boot sandbox + pull image (10–30s)
            → restore volume (2–5s) → ready
 Total: 15–35s of the user staring at a spinner, every single time.
  • Pro: zero idle cost — you allocate compute only when needed, nothing sits warm.
  • Con: cold start is dominated by booting and image pull, which is far too slow for an interactive "click and code" product. The user feels every second.
  • Verdict: avoid as the default open path. Acceptable only as a fallback when the warm pool is empty. The boot cost belongs off the critical path.
Warm pool — keep pre-booted sandboxes ready (recommended)

Maintain a small pool of pre-booted, generic sandboxes sitting idle and ready. When a user opens a workspace, grab one from the pool, restore their volume into it, and hand it over — skipping the slow boot and image-pull entirely. A background process keeps refilling the pool as sandboxes are consumed.

Worked example:

 Pool holds N pre-booted sandboxes (booted, base image already present)
 click open → claim a warm sandbox (instant)
            → restore volume into it (2–5s)  ← only the volume restore remains
            → ready in ~2–5s
 Background: pool drops to N-1 → refill another → back to N
  • Pro: cold start drops from tens of seconds to a few, because boot and image pull happen before the user clicks, not after. The user feels only the volume restore.
  • Con: the pool costs some idle compute (you pay to keep N sandboxes warm), and you must size N for peak open-rate or it empties under a burst. Sizing is a forecasting problem. A warm pool is also typically segmented per base image: a workspace whose image_ref isn't pre-booted falls back to on-demand (or to a per-language pool), so you size a pool per base image and multiply by the number of images you keep warm.
  • Verdict: the recommended pattern for interactive cold start. The pool is small relative to total workspaces — you're paying for the rate of opens, not for all 50M workspaces — so it's affordable, and it's the difference between "instant" and "slow."

Tip

Separate the two parts of cold start. Boot + image pull is generic and can be done ahead of time (warm pool). Volume restore is per-user and must happen at open time. Only the second part is unavoidably on the critical path, which is why warming the first part is so effective — and why fast volume restore (deep dive 3) is the other half of the cold-start story.

Hibernation / suspend of idle workspaces. This is the cost lever. A workspace with no activity for some minutes is suspended: snapshot its filesystem, tear down the sandbox, return its CPU and memory to the pool. The workspace now costs only the disk holding its volume — pennies, not dollars. When the user comes back, it's reopened from the snapshot. This is what makes 50M workspaces affordable: at any moment you only pay compute for the ~100K that are actually open. The idle timeout is a tradeoff — shorter saves more money but makes "come back after lunch" slower (a resume instead of an already-running workspace); longer keeps things instant but wastes compute on workspaces nobody's using.

Routing the browser to the right host. A running workspace lives on exactly one host, and the browser's persistent connection (terminal, file I/O) must reach that host — not a random one behind a load balancer. So you need a routing registry mapping workspace_id → host/endpoint, written when a workspace is placed and read when the browser connects. This is the same shape as a chat app's user → connection server registry: a stateful, host-specific connection means you can't just round-robin requests; you must route to the one host that holds the session. When a workspace suspends, its registry entry is removed; when it reopens (possibly on a different host), a new entry is written. The browser always asks the control plane "where is my workspace?" and connects to the returned endpoint.

Warning

Don't put the per-workspace WebSocket behind a normal stateless load balancer that can send the browser to any host. The terminal and file I/O only exist on the one host running the sandbox; a misrouted connection reaches a host that has no idea about this workspace. Route by workspace_id to the placed host, and re-resolve the endpoint after any resume (the host may have changed).

3. Persistence — keeping the filesystem alive across ephemeral compute

The compute is throwaway — suspended and rebuilt constantly — but the user's files must survive. The whole trick is to separate durable storage from the ephemeral sandbox so the files outlive any particular container or microVM.

Separate the volume from the compute. The workspace's filesystem lives on a durable volume that is independent of the sandbox running it — either a network/block volume that can be detached and reattached, or a snapshot stored in object storage. The sandbox is empty at rest; it only mounts the volume while running. So tearing down the sandbox loses nothing — the files are on the volume, not in the container.

Snapshot on suspend, restore on resume.

 SUSPEND:
   1. flush pending writes to the volume
   2. snapshot the volume (point-in-time durable copy)
   3. write snapshot to durable storage (block snapshot or object store)
   4. tear down the sandbox, free compute

 RESUME:
   1. claim a (warm) sandbox
   2. restore the latest snapshot as the workspace's volume
   3. mount it into the sandbox
   4. user sees their files exactly as they left them

The snapshot is what makes suspend/resume safe: it's a consistent, durable picture of the filesystem at the moment of suspend, replicated across machines so a host failure can't lose it. Restore is the per-user part of cold start (deep dive 2) — keeping snapshots fast to restore (incremental snapshots, lazy/copy-on-read loading so the workspace is usable before every byte is fetched) directly improves resume time.

Filesystem vs running-process state — the hard tradeoff. There are two things you could try to persist: just the files on disk, or the whole live machine including memory and running processes. The most ambitious option is to snapshot everything — so a resumed workspace picks up mid-run with its dev server still going. It sounds ideal, but capturing live memory and process state is genuinely hard (CRIU-level checkpoint/restore of a running system), heavy, and expensive at scale. Work up to why the practical answer is to persist the filesystem only:

Snapshot the full running machine state (memory + processes) too (situational)

Snapshot not just the disk but the live memory and process state of the VM, so on resume the running dev server and every process pick up exactly where they were — a true suspend/resume of the whole machine.

Worked example:

 Suspend: snapshot disk AND RAM (the running :3000 server's memory)
 Resume: restore disk + memory → the server is STILL running, mid-request even
   → user notices nothing; processes never stopped from their point of view
  • Pro: the best possible experience — nothing to restart, no lost in-memory state. Feels like the workspace never left.
  • Con: much harder and heavier. A memory snapshot is large (gigabytes of RAM per workspace) and slow to write and restore; open network connections and external state don't snapshot cleanly; it raises cost and complexity sharply. Doing it for millions of workspaces is expensive.
  • Verdict: situational — a premium feature for fast resume, not the baseline. Most platforms persist the filesystem only and accept that processes restart, because the cost and complexity of memory snapshots rarely justify the benefit at scale.
Persist the filesystem only — restart processes on resume (recommended, practical)

Snapshot just the files on disk. On resume, the filesystem is exactly as it was, but any running processes are gone — the dev server, the running test, the shell's in-memory state. The user (or a startup script) restarts them.

Worked example:

 Before suspend: editing files + a `npm run dev` server running on :3000
 Suspend: snapshot /app (the files) → tear down
 Resume: files back exactly as left; the :3000 server is NOT running
   → a startup hook re-runs `npm run dev`, server back in seconds
  • Pro: simple, robust, and cheap. Filesystem snapshots are a well-understood, fast operation. Nothing exotic. This is what real platforms do.
  • Con: running processes don't survive — the user must restart their server / re-run their build. In-memory state (a long REPL session, an unsaved process buffer) is lost.
  • Verdict: the practical, recommended default. Persist files; treat processes as restartable. Pair it with a startup script so common processes (the dev server) come back automatically, making the loss mostly invisible.

Caution

Do not store the user's files inside the ephemeral container's writable layer with no separate durable volume. When the container is torn down on suspend — which happens constantly — the files vanish. The volume must be a separate durable thing, snapshotted to replicated storage, so compute can be destroyed and rebuilt freely without ever risking the files.

What is lost on a crash, and how reconnect rebuilds state. If a host crashes while a workspace is running, anything written since the last durable flush can be lost. This is the flip side of edits "feeling local": keystrokes hit a fast local working filesystem on the host (so editing has no per-write round trip), and that local state is flushed and replicated to the durable volume asynchronously/periodically rather than synchronously on every keystroke. That asynchrony is exactly why a crash can lose the last few seconds — the durable copy lags the local one. Keep the flush interval short so the on-disk durable state stays close to current; the crash then loses at most that small window. On reconnect, the control plane sees the placement is dead, schedules the workspace onto a new host, restores the latest volume, and returns the new endpoint — the browser re-resolves where its workspace lives and reconnects there. Running processes are gone (per the recommended model), so a startup hook restarts the dev server. The durable volume — not the live sandbox — is the source of truth, so a crash degrades to "reopen from the last durable state," not "work lost."

Tradeoffs & bottlenecks

  • Isolation strength vs density/cost. Stronger boundaries (microVM, full VM) cost more memory and pack fewer workspaces per host than plain containers; weaker boundaries are cheaper but unsafe for untrusted code. Since the code is always untrusted, you pay for a strong boundary (gVisor or microVM) — the central, unavoidable tradeoff.
  • Warm pool size vs idle cost. A bigger warm pool means faster cold start under bursts but more idle compute you pay for; too small and a spike of opens drains it, falling back to slow on-demand boots. Sizing is a forecast against your open-rate.
  • Idle timeout vs resume cost. A short idle timeout reclaims compute aggressively (saves money) but makes "come back later" a slower resume; a long timeout keeps things instant but wastes compute on inactive workspaces.
  • Filesystem-only vs full-machine snapshots. Persisting only files is cheap and robust but loses running processes; snapshotting memory keeps processes alive but is heavy and expensive. Most platforms choose filesystem-only plus restart hooks.
  • Compute/storage separation. Splitting durable volumes from ephemeral sandboxes is what makes suspend/resume and cheap idle possible, at the cost of a volume restore on every resume (a per-user step on the cold-start path) — worth it, and mitigated by incremental/lazy restore.
  • Routing to the right host. A workspace lives on one host, so the connection is stateful and host-specific; the routing registry must stay correct across placements and resumes, and re-resolve after a host change — a freshness problem like a chat app's connection registry.
  • Per-host capacity hotspots. A few heavy workspaces (big builds, memory-hungry tools) on one host can crowd it; the scheduler must place by real resource need and rebalance, and cgroup limits keep one workspace from taking the whole host.

Extensions if asked

If the interviewer wants to push further, add only the extension that changes the design discussion (these are standalone topics; there's no dedicated guide for each yet):

  • Real-time collaborative editing. Two users editing the same file at once, with character-by-character merging — operational transforms or CRDTs over a live document. This is a different problem from running a sandbox and is the headline out-of-scope item; flag it unless the interviewer specifically wants it.
  • Language servers (LSP) and IDE intelligence. Autocomplete, go-to-definition, and diagnostics run a language server inside the workspace sandbox (it needs the user's code and dependencies) and stream results to the browser editor — additional process load per workspace, but the same isolation model.
  • Port forwarding / preview URLs. Exposing a workspace's running web server (e.g. on port 3000) at a public preview URL means proxying external traffic into one tenant's sandbox safely — a routing and isolation problem, since you're now letting outside traffic reach untrusted code.
  • Prebuilds / dependency caching. Pre-running the slow setup (installing dependencies, building) ahead of time so opening a project is fast even on first use — a build-and-snapshot pipeline feeding the volume.
  • GPU / heavy workloads. Workspaces that need GPUs or large memory change the scheduling and cost model (GPUs are scarce and expensive, so pooling and idle reclaim matter even more).

What interviewers look for & common mistakes

What interviewers usually reward:

  • Leading with isolation of untrusted code — naming container escape as the threat and choosing a strong boundary (gVisor or a Firecracker microVM) over a plain shared-kernel container, with the reasoning.
  • cgroup resource limits (CPU/memory/disk quotas) and network egress restrictions as part of isolation, so one tenant can't starve or attack others.
  • The cost reality — recognizing you can't keep millions of idle workspaces running, so idle workspaces are suspended and compute reclaimed.
  • The warm-pool pattern for fast cold start, separating generic boot (warmable) from per-user volume restore (on the critical path).
  • Separating durable storage from ephemeral compute, with snapshot-on-suspend / restore-on-resume, and the filesystem-only-vs-full-machine tradeoff.
  • Routing the browser to the one host holding the workspace via a workspace → host registry, re-resolved after a resume.

Before you finish, do a quick mistake check:

  • Did you treat running untrusted code as the headline, and choose a strong sandbox (not a plain container) with container escape as the named threat?
  • Did you add cgroup CPU/memory/disk quotas and restrict network egress, so one tenant can't starve or attack others?
  • Did you recognize that idle workspaces must be suspended (the cost reality), not kept running?
  • Did you use a warm pool to make cold start fast, and explain what part of cold start it removes?
  • Did you store the filesystem on a separate durable volume, snapshotted, so compute can be destroyed and rebuilt without losing files?
  • Did you say what's lost on a crash or suspend (running processes) and how a reconnect restores the workspace from the durable volume?
  • Did you route the browser's persistent connection to the specific host running the workspace, and re-resolve it after a resume?

Practice this live

Run this exact question with our AI voice interviewer and get feedback.

Start the interview →