Sandboxes
Sandboxes are short-lived Linux environments you can provision on demand, run commands in, and destroy.
Each sandbox is scoped to a Railway environment and runs on Railway's virtual machine primitive, giving you isolated, on-demand compute for anything you'd run on a VM.
How it works
Each sandbox is a programmatically controllable, fully isolated virtual machine. You create one, run commands against it with exec, and destroy it when you're done. Sandboxes start from a clean Debian base and are ready to exec against once Sandbox.create() resolves.
Dashboard
Navigate to the Sandboxes tab in your project to view, create, and destroy sandboxes.
You can SSH into a running sandbox directly without leaving the dashboard. If you have SSH keys configured in your Railway account, you can also copy the SSH command from the dashboard to use in your terminal of choice.
To add SSH keys to your account, go to Account Settings -> SSH Keys.
TypeScript SDK
The SDK is the primary interface for working with sandboxes programmatically. It's open source on GitHub.
Note: The SDK is under active development while sandboxes are in Priority Boarding, and its API may change in breaking ways between releases.
Installation
Or scaffold a new project with the SDK preconfigured:
Quick start
Sandbox.create() resolves once the sandbox is RUNNING and ready to accept commands.
Creating a sandbox
These static methods cover every way to get a Sandbox:
Running commands
exec starts a command and returns a handle. Await the handle to get the final result. exec doesn't throw on a non-zero exit code, so inspect exitCode instead.
Pass timeoutSec to kill the command after a deadline and resolve with timedOut: true. Without it, the command runs until it exits, so you can run long-lived processes like agents, dev servers, and builds. See Long-running commands to stream their output and detach or reattach the session.
Set cwd to run the command in a directory, and env to layer extra environment variables over the sandbox's own:
The command fails if cwd doesn't exist. Per-command env values travel inside the command string, so they're visible to ps inside the sandbox. For secrets, set env when you create the sandbox instead. Both options apply to fresh commands only, and are rejected when you reattach to a session by name.
Long-running commands
A command started with exec runs on the sandbox independently of the client that started it. The handle exposes a durable sessionName, so you can stop streaming a command and reattach to it later, even from a different process. This suits agents, dev servers, and any job that outlives a single connection.
Stream output as it arrives with the onStdout and onStderr callbacks:
To stop following the output without ending the command, call detach(). It resolves with the session name. The command keeps running, and you reattach later by passing that name back to exec.
Reattaching replays the output retained for the session, then follows it live. Pass resumeFromLastRead: true to receive only the output produced since the server's last read, instead of replaying from the start.
Call handle.kill(signal) to terminate a running command. It sends the signal (TERM by default) to the command's process group, and the handle then resolves with the command's exit. Detaching leaves the command running. Killing ends it.
Files
sandbox.files reads and writes files on a running sandbox's filesystem.
| Method | Description |
|---|---|
read | Read a file as text, bytes, or a stream |
write | Create or overwrite a file, creating parent directories |
list | List a directory's entries |
stat | Get metadata for a path |
exists | Check whether a path exists |
mkdir | Create a directory, including parents |
rename | Move or rename a file or directory |
remove | Delete a file or empty directory |
Reading and writing
Write a file and read it back:
write creates the file and creates missing parent directories automatically.
Files are created with mode 0644 by default. Pass mode to set permissions, for example to make a script executable:
write accepts several content types, which differ in how they recover from a dropped connection:
write input | How it uploads |
|---|---|
string, Uint8Array, ArrayBuffer, Blob | Buffered, retried automatically on a dropped connection |
ReadableStream, AsyncIterable<Uint8Array> | Streamed without buffering, one-shot (not retried) |
() => stream (a factory) | Streamed without buffering, retried by calling the factory again |
Streaming lets you push a file larger than memory. A bare stream isn't retried, so an error mid-transfer fails with RailwayConnectionError and can leave a partial file.
Pass a factory so a retry reads a fresh stream:
Read a file as a stream to pull large files without filling memory, or read a byte range. Use offset and length to read from the start, or fromEnd with length to tail the end of a file:
Listing
List a directory and read each entry's metadata:
Removing
remove deletes a single file or empty directory. For recursive deletes, run sandbox.exec("rm -rf ...").
Reading a missing path throws SandboxFileNotFoundError. Other failures throw SandboxFilesError, which reports the failing operation and path.
Destroying a sandbox
Use destroy() for explicit teardown, or await using to destroy automatically when the scope exits, even on throw.
Reconnecting
A sandbox outlives the process that created it. Reattaching by id is useful in serverless and multi-step workflows.
Call sandbox.refresh() to re-read the sandbox and update its status and other fields in place.
Templates
A template is a reusable base: an ordered list of build steps that Railway builds once, content-addresses, and caches. Creating a sandbox from a template forks that cached build instead of starting from scratch.
This lets you start a sandbox from a pre-built base, saving spin-up time.
SandboxTemplate is immutable. Every method returns a new template, so a base can branch into variants without mutation.
The template builder exposes these methods:
| Method | Effect |
|---|---|
.run(command) | Add a raw build step |
.withPackages(...names) | Install Debian packages via apt-get |
.withEnv({ KEY: "value" }) | Set environment variables for subsequent steps |
.workdir(dir) | Set the working directory for subsequent steps |
.build(options?) | Build and cache the template ahead of time |
Sandbox.create(template) builds the template automatically. Call .build() explicitly only to pre-warm the cache before the first create.
Forking
Forking clones a running sandbox's filesystem into a new, independent sandbox. The fork boots fresh from a copy of the source's disk, so files are preserved but running processes and memory are not. Use it to branch a sandbox after expensive setup, for example installing dependencies once and forking per task.
Sandbox.create(source) is equivalent to source.fork(). The source must be RUNNING, and the fork is created in the same environment. Pass idleTimeoutMinutes or networkIsolation to set them on the fork, which doesn't inherit them from the source.
Checkpoints
A checkpoint is a named snapshot of a sandbox's disk, stored server-side in the environment. Capture one from a running sandbox after expensive setup, then boot new sandboxes from it by name, from any process with access to the environment.
checkpoint(name) requires a running sandbox, which keeps running during the capture. The call resolves once the checkpoint is bootable, so a sandbox with a lot of changed disk takes longer to capture. Reusing a name replaces that checkpoint with the new capture.
Like a fork, a sandbox created from a checkpoint boots fresh from a copy of the disk: files are preserved, running processes are not. Unlike a fork, the source sandbox doesn't need to exist anymore. Use a checkpoint for a base you reuse across sessions, and a fork to branch a live sandbox.
Manage the environment's checkpoints with static methods:
Each entry's key field holds the checkpoint name, and renameCheckpoint and deleteCheckpoint take that name. Deleting a checkpoint also deletes its underlying disk snapshot.
The number of checkpoints an environment can hold matches its plan's sandbox limit, listed in Sandbox limits. Checkpoints are counted separately from running sandboxes, and replacing a checkpoint by reusing its name doesn't increase the count.
Configuration
token and environmentId each resolve in order: an explicit option in the configuration object, then an environment variable.
| Option | Environment variable |
|---|---|
token | RAILWAY_API_TOKEN |
environmentId | RAILWAY_ENVIRONMENT_ID |
Pass explicit values to override:
idleTimeoutMinutes sets how long a sandbox can sit idle before Railway automatically destroys it. Set it high enough to cover the gaps between steps in reconnect workflows, and low enough to avoid paying for idle compute. Without it, the sandbox uses the plan default. The default and allowed range depend on your plan, so see Idle timeout for the per-plan values.
env bakes environment variables into the sandbox, available to every command over both exec and SSH for the sandbox's lifetime. Use it for values a command needs at runtime, including secrets. Values can reference other Railway variables, for example ${{shared.NPM_TOKEN}}, resolved when the sandbox is created. create and fork both accept env, and a fork doesn't inherit the source's variables.
Examples
For complete, runnable code, see the sandbox examples in the SDK repository.
CLI
The Railway CLI can create, fork, connect to, run commands in, forward ports into, and destroy sandboxes, build templates, capture and boot from checkpoints, and seed variables at create time. See railway sandbox for all subcommands and options.
Sandbox limits per environment
Each environment can run a fixed number of sandboxes at once, based on your workspace's plan. The cap applies per environment, not per project or workspace.
| Plan | Sandboxes per environment |
|---|---|
| Trial | 10 |
| Free | 10 |
| Hobby | 50 |
| Pro | 100 |
Enterprise workspaces share the Pro cap of 100 sandboxes per environment.
Only sandboxes in the CREATING or RUNNING state count toward the cap. Destroyed sandboxes don't. Creating a sandbox past the cap fails with an error.
Timeouts and output
A sandbox enforces an idle timeout: how long it can sit idle before Railway destroys it. Commands have a separate, optional timeout.
| Limit | Default | Maximum |
|---|---|---|
| Idle timeout (Hobby and Pro) | 30 minutes | 120 minutes |
| Idle timeout (Trial and Free) | 5 minutes | 5 minutes |
The idle timeout default and maximum depend on your plan, as shown above.
In the CLI, railway sandbox exec streams output live and runs the command until it exits, unless you pass --timeout <SECONDS>, a client-side deadline. When the deadline expires, the command receives SIGTERM and exec exits with code 124. Use --detach to start a command in the background and get a session name back, then --session <name> to reattach later. In the SDK, a command has no timeout unless you set timeoutSec, so long-running commands keep going until they exit. The truncated field on an exec result reports when captured output was cut short.
Idle timeout
A sandbox is considered idle when you haven't interacted with it for longer than its idle timeout. Interacting means running a command (exec) or sending a command over an SSH session. Every interaction resets the timer, so the countdown always starts from your most recent interaction.
The idle timeout only counts your interactions with the sandbox, not anything running inside it. A process, server, or job running in the sandbox doesn't keep it alive on its own. Once a sandbox stays idle past its timeout, Railway shuts it down automatically.
Set the idle timeout with idleTimeoutMinutes in the SDK or --idle-timeout-minutes in the CLI. On the Hobby and Pro plans it defaults to 30 minutes and can be set from 1 to 120 minutes. On the Trial and Free plans it defaults to 5 minutes and can be set from 1 to 5 minutes. Setting a value above your plan's maximum returns an error.
Networking
Every sandbox has outbound internet access through a NAT gateway. Whether it can reach the rest of your environment over the private network depends on its network isolation mode, set when you create or fork it.
| Mode | Behavior |
|---|---|
ISOLATED | Default. Outbound internet access only. The sandbox can't reach other services in the environment over private networking, and they can't reach it. |
PRIVATE | The sandbox joins the environment's private network and keeps outbound internet access. It can reach other services over private networking, for example postgres.railway.internal, and they can reach it. |
A sandbox is ISOLATED unless you opt into PRIVATE, so existing sandboxes are unaffected.
In the SDK, pass networkIsolation when you create or fork a sandbox, and read it back from sandbox.networkIsolation:
In the CLI, pass --private-network to railway sandbox create or railway sandbox fork.
To run commands in a sandbox in either mode, use exec or SSH, and to move files in and out, use the files API. To reach a server running inside the sandbox from your machine, forward its port with railway sandbox forward.
Pricing
Sandboxes are billed by resources (CPU, memory, network egress) consumed. You pay only for what a sandbox uses while it runs, so destroying sandboxes when you're done, or setting a short idle timeout, keeps costs down. Idle sandboxes still consume resources that we bill for.
Sandbox VM resources are billed at
| Resource | price |
|---|---|
| Memory | $0.00000001929012 MB•second ($50 GB / month) |
| vCPU | $0.00000001929012 vCPU•second ($50 vCPU / month) |
| Egress | $0.05 GB |