Skip to main content
Sandboxes are instant-launching virtual machines serving as sandboxed compute runtimes for agents. You can securely run LLM-generated code inside these VMs making them ideal for agents that need access to an operating system to run commands with no risk of escaping. They provide a basic REST API interface for accessing the file system and processes, along with an MCP server that makes these capabilities available as tool calls. Unlike traditional sandbox infrastructure, Blaxel’s standout feature is fully managed lifecycle. Sandboxes resume from standby in under 25 milliseconds and automatically scale to zero after a few seconds of inactivity. This means that your sandboxes wait on standby indefinitely when not used, eliminating cold starts without complex orchestration. Memory state is maintained even after scaling down, including the running processes and entire filesystem. For cost-effective long-term persistence, you can attach volumes to sandboxes. Lifecycle of Blaxel Sandboxes In addition to automatic scale-to-zero, Blaxel also supports automatic sandbox deletion based on expiration policies. Starter quotas enforce time-to-live (TTLs), while higher quota tiers unlock sandboxes with unlimited persistence.

Use cases

Some examples of use cases include:
  • Code review agents that analyze repositories to detect the effects of changes. These agents run fully isolated compute environments for each tenant while keeping them snapshotted in standby between sessions, eliminating the need to clone the repo every time.
  • Code generation agents that iterate in their own compute environments, and instantly render live application previews as human users build, step away, and log back in.
  • Data analyst agents that execute adhoc data analysis workflows, generating scripts on-the-fly and running them securely against private files or data within an isolated, ZDR-compliant environment.
  • Background agents that operate beyond their pre-configured tools. Each agent gets its own “personal computer” where it can autonomously install packages, execute custom scripts, store files, and adapt to new requirements securely. They can parallelize dozens of those personal computers.

Create a sandbox

Using the SDKs

Create a new sandbox using the Blaxel SDK by specifying a name, image to use, optional deployment region, optional labels, and the ports to expose. Note that ports 80 (system), and 443 & 8080 (sandbox API) are reserved by Blaxel.
The Blaxel SDK authenticates with your workspace using credentials from these sources, in priority order:
  1. when running on Blaxel, authentication is handled automatically
  2. variables in your .env file (BL_WORKSPACE and BL_API_KEY, or see this page for other authentication options).
  3. environment variables from your machine
  4. configuration file created locally when you log in through Blaxel CLI (or deploy on Blaxel)
When developing locally, the recommended method is to just log in to your workspace with Blaxel CLI. This allows you to run Blaxel SDK functions that will automatically connect to your workspace without additional setup. When you deploy on Blaxel, this connection persists automatically.When running Blaxel SDK from a remote server that is not Blaxel-hosted, we recommend using environment variables as described in the third option above.
import { SandboxInstance } from "@blaxel/core";

// Create a new sandbox
const sandbox = await SandboxInstance.create({
  name: "my-sandbox",
  image: "blaxel/base-image:latest",   // public or custom image
  memory: 4096,   // in MB
  ports: [{ target: 3000, protocol: "HTTP" }],   // optional; ports to expose
  labels: { env: "dev", project: "my-project" }, // optional; labels
  region: "us-pdx-1"   // optional; if not specified, Blaxel will choose a default region
});
An alternative is to use the helper function createIfNotExists() (TypeScript) / create_if_not_exists() (Python). This helper function either retrieves an existing sandbox or creates a new one if it doesn’t exist. Blaxel first checks for an existing sandbox with the provided name and either retrieves it or creates a new one using your specified configuration.
import { SandboxInstance } from "@blaxel/core";

// Create sandbox if it doesn't exist
const sandbox = await SandboxInstance.createIfNotExists({
  name: "my-sandbox",
  image: "blaxel/base-image:latest",  // public or custom image
  memory: 4096,    // in MB
  ports: [{ target: 3000, protocol: "HTTP" }],  // optional; ports to expose
  labels: { env: "dev", project: "my-project" }, // optional; labels
  region: "us-pdx-1"    // optional; if not specified, Blaxel will choose a default region
});

Using the CLI and Console

Although less common, it is also possible to create a sandbox via the Blaxel CLI or the Blaxel Console.
bl new sandbox my-sandbox
cd my-sandbox
bl deploy
This command initializes a new sandbox project and configuration file in the named directory my-sandbox and then deploys the sandbox on Blaxel.The project directory contains the Blaxel configuration file blaxel.toml, which can be further customized to suit your sandbox deployment requirements, by modifying the base image, memory, environment, etc. Learn more about the blaxel.toml file.
Running bl deploy here also saves the image to be reused later as a template.

Understand sandbox configuration

Images

The list of public images can be found here. To create a sandbox with one of those images, enter blaxel/{NAME}:latest (e.g. blaxel/nextjs:latest).
Custom sandbox images (or templates) enable you to create sandboxes with a consistent, customized set of tools, configurations, or entrypoint scripts.

Memory and filesystem

For maximum performance, Blaxel sandboxes store part of their filesystem in memory. The base of the filesystem (the user-supplied image) is stored as read-only files on host storage using a highly-efficient format called EROFS (Extendable Read-Only File System). On top of the read-only base, a writable layer lives entirely in the sandbox’s RAM using tmpfs. OverlayFS serves as orchestrator, directing reads to the EROFS base and writes to the in-memory tmpfs filesystem. Due to this, Blaxel sandboxes reserve, when possible, approximately 50% of the available memory for the tmpfs filesystem. More information on our implementation is available in this blog post. To avoid out-of-memory errors or if additional storage is required, one option is to add storage using volumes. However, this requires deleting and recreating the sandbox first. In addition, volumes are not as fast as the native in-memory filesystem.

Ports

The following ports are reserved by Blaxel’s system:
  • 443: This port hosts the main sandbox API and is exposed via HTTPS
  • 80: Reserved for system operations
  • 8080: Reserved for sandbox API functionality
You can expose specific non-reserved ports when creating a new sandbox by using the ports parameter. This allows you to access these ports from outside the sandbox.

Regions

Select the region where you want to deploy your sandbox. If you don’t specify a region, Blaxel will automatically use a default region.

Labels

You can also add optional labels for sandboxes. Labels are specified as key-value pairs during sandbox creation.
// Create a new sandbox
const sandbox = await SandboxInstance.create({
  name: "my-sandbox",
  image: "blaxel/base-image:latest",   // public or custom image
  memory: 4096,   // in MB
  labels: { env: "dev", project: "my-project" }, // optional; labels
  region: "us-pdx-1"   // optional; if not specified, Blaxel will choose a default region
});
You can use these labels for filtering sandboxes in the Blaxel CLI or Blaxel Console:
# Get sandboxes with specific label (e.g., env=dev)
bl get sandboxes -o json | jq -r '.[] | select(.metadata.labels.env == "dev") | .metadata.name'

Expiration

Blaxel supports automatic sandbox deletion based on specific conditions.

Retrieve an existing sandbox

To reconnect to an existing sandbox, simply provide its name:
import { SandboxInstance } from "@blaxel/core";

// Connect to existing sandbox
const sandbox = await SandboxInstance.get("my-sandbox");
Complete code examples demonstrating all operations are available on Blaxel’s GitHub: in TypeScript and in Python.

Delete a sandbox

Delete a sandbox by calling:
  • the class-level delete() method with the sandbox name as argument, or
    import { SandboxInstance } from "@blaxel/core";
    
    // Delete sandbox using class-level method
    await SandboxInstance.delete("my-sandbox");
    
  • by calling the instance-level delete() method:
    import { SandboxInstance } from "@blaxel/core";
    
    // Get existing sandbox
    const sandbox = await SandboxInstance.get("my-sandbox");
    
    // Delete sandbox using instance-level method
    await sandbox.delete()
    

Upgrade a sandbox’s API

Every Blaxel sandbox includes a custom API binary, which is necessary for sandbox functionality like process management and file operations. It is possible to perform an in-place upgrade of this API without needing to recreate or restart the sandbox.
This feature is currently in beta and only available for sandboxes built or created with sandbox API v0.2.0 or later (sandboxes created after 2 Feb 2026). For sandboxes built or created earlier than this date/API version, in-place upgrade is not possible; the sandbox must be recreated to obtain the new API.
import { SandboxInstance } from "@blaxel/core";

// Connect to existing sandbox
const sandbox = await SandboxInstance.get("my-sandbox")

// Upgrade sandbox API to "latest"
await sandbox.system.upgrade({ version: "latest" })
You can find a list of available API versions in Blaxel’s public repository.

Connect to a sandbox with an interactive terminal

You can explore the contents of a sandbox with an interactive terminal. You can access this terminal in two ways:
  • From the Blaxel Console, by visiting the detail page for your sandbox in your web browser and selecting the Terminal tab: image.png
  • From your local host, by running:
    bl connect sandbox your-sandbox-name
    
    image.png

Manage sandbox hibernation

Sandboxes stay active as long as there’s an active connection to them, typically through a WebSocket connection. When a browser tab becomes inactive, the WebSocket should disconnect after some time, but this behavior depends on the specific browser implementation. There are no built-in stop or start functions available in the SDKs to force hibernation. This means you’ll need to rely on other approaches to better control when sandboxes remain active:
  1. Hide iframe when tab is inactive Use JavaScript events to detect when a user is not active on the tab:
    • Listen for tab visibility events that indicate when the user switches away from your tab
    • When the user becomes inactive, hide the iframe containing the sandbox preview
    • Show the iframe again when the user returns to the tab
  2. Implement auto-disconnect on tab switch Some browsers (like Chrome) may keep WebSockets alive even when tabs are inactive to improve performance. You can implement an auto-disconnect feature that:
    • Detects when users switch tabs
    • Automatically disconnects the WebSocket connection
    • Reconnects when the user returns
  3. Use activity-based timeouts Set up a timer system in your interface that:
    • Monitors user activity (typing, interactions, etc.)
    • Hides the preview after a period of inactivity
    • Prompts the user to confirm they’re still using the sandbox
You can use the SDKs from the frontend if you have the right headers set on the session. Use the sandbox.sessions.create function to manage sessions. Here is an example of auto disconnect on tab switch with Vite through a plugin:
The code below is illustrative and not intended for production use.
// vite.config.ts
import { defineConfig } from 'vite'
import { hmrVisibility } from './vite-plugin-hmr-visibility'

// https://vite.dev/config/
export default defineConfig({
  plugins: [
    hmrVisibility({
      disconnectDelay: 2000, // Wait 2 seconds before disconnecting
      debug: true, // Enable debug logging
    }),
  ],
  server: {
    // Prevent Vite from aggressive reconnection attempts
    hmr: {
      overlay: false, // Disable error overlay which can trigger reconnections
    },
  },
})

// vite-plugin-hmr-visibility.ts
/**
 * Vite Plugin: HMR Visibility Manager
 * Automatically disconnects/reconnects HMR websocket based on tab visibility
 * to reduce serverless billing costs
 */

import type { Plugin } from 'vite'

export interface HMRVisibilityOptions {
  /**
   * Grace period before disconnecting (ms)
   * @default 2000
   */
  disconnectDelay?: number

  /**
   * Enable debug logging
   * @default false
   */
  debug?: boolean
}

export function hmrVisibility(options: HMRVisibilityOptions = {}): Plugin {
  const {
    disconnectDelay = 2000,
    debug = false,
  } = options

  return {
    name: 'vite-plugin-hmr-visibility',
    apply: 'serve', // Only apply in dev mode

    transformIndexHtml() {
      // Inject the HMR visibility manager as an inline script
      return [
        {
          tag: 'script',
          attrs: { type: 'module' },
          children: `
// HMR Visibility Manager - Disconnects HMR when tab is hidden
(function() {
  const log = (...args) => {
    if (${JSON.stringify(debug)}) {
      console.log('[HMR Visibility]', ...args);
    }
  };

  let disconnectTimer = null;
  let isTabVisible = !document.hidden;
  const DISCONNECT_DELAY = ${JSON.stringify(disconnectDelay)};

  // Store reference to original functions
  const OriginalWebSocket = window.WebSocket;
  const OriginalSetTimeout = window.setTimeout;
  const OriginalSetInterval = window.setInterval;

  let hmrSocket = null;
  let shouldBlockReconnect = false;
  let blockedTimers = new Set();

  // Patch setTimeout and setInterval to block Vite's reconnection timers
  window.setTimeout = function(callback, delay, ...args) {
    // Block timers when tab is hidden and it looks like a reconnection attempt
    if (shouldBlockReconnect && delay && delay >= 500 && delay <= 5000) {
      const callbackStr = callback.toString();
      if (callbackStr.includes('connect') || callbackStr.includes('WebSocket') || callbackStr.includes('ws')) {
        log('Blocked reconnection timer (setTimeout)');
        const timerId = OriginalSetTimeout(() => {}, 999999999);
        blockedTimers.add(timerId);
        return timerId;
      }
    }
    return OriginalSetTimeout.call(this, callback, delay, ...args);
  };

  window.setInterval = function(callback, delay, ...args) {
    // Block intervals when tab is hidden
    if (shouldBlockReconnect) {
      const callbackStr = callback.toString();
      if (callbackStr.includes('connect') || callbackStr.includes('WebSocket') || callbackStr.includes('ws')) {
        log('Blocked reconnection interval (setInterval)');
        const timerId = OriginalSetInterval(() => {}, 999999999);
        blockedTimers.add(timerId);
        return timerId;
      }
    }
    return OriginalSetInterval.call(this, callback, delay, ...args);
  };

  // Patch WebSocket to track and block HMR connections
  window.WebSocket = function(url, protocols) {
    // Block new WebSocket connections when tab is hidden
    if (shouldBlockReconnect) {
      log('Blocked WebSocket connection attempt while tab is hidden:', url);
      // Return a fake WebSocket that stays in CONNECTING state forever
      const fakeWs = {
        readyState: 0, // CONNECTING (keeps Vite waiting)
        close: () => { log('Fake WebSocket close called'); },
        send: () => { log('Fake WebSocket send called'); },
        addEventListener: () => {},
        removeEventListener: () => {},
        dispatchEvent: () => false,
        onopen: null,
        onclose: null,
        onerror: null,
        onmessage: null,
      };
      return fakeWs;
    }

    const ws = new OriginalWebSocket(url, protocols);

    // Track HMR WebSocket and intercept its event handlers
    if (typeof url === 'string') {
      hmrSocket = ws;
      log('HMR WebSocket created and tracked');

      // Intercept the onclose setter to control reconnection behavior
      let userOnClose = null;
      Object.defineProperty(ws, 'onclose', {
        get() { return userOnClose; },
        set(handler) {
          userOnClose = function(event) {
            // If we're blocking reconnect, don't call Vite's onclose handler
            if (shouldBlockReconnect) {
              log('Suppressed onclose handler - blocking reconnection');
              return;
            }
            // Otherwise, call the original handler
            if (handler) {
              handler.call(this, event);
            }
          };
        },
        configurable: true
      });

      // Also intercept addEventListener for 'close' events
      const originalAddEventListener = ws.addEventListener;
      ws.addEventListener = function(type, listener, options) {
        if (type === 'close') {
          const wrappedListener = function(event) {
            if (shouldBlockReconnect) {
              log('Suppressed close event listener - blocking reconnection');
              return;
            }
            listener.call(this, event);
          };
          return originalAddEventListener.call(this, type, wrappedListener, options);
        }
        return originalAddEventListener.call(this, type, listener, options);
      };
    }

    return ws;
  };

  // Copy static properties from original WebSocket
  Object.setPrototypeOf(window.WebSocket, OriginalWebSocket);
  window.WebSocket.prototype = OriginalWebSocket.prototype;

  // Copy static constants
  Object.defineProperty(window.WebSocket, 'CONNECTING', { value: 0, enumerable: true });
  Object.defineProperty(window.WebSocket, 'OPEN', { value: 1, enumerable: true });
  Object.defineProperty(window.WebSocket, 'CLOSING', { value: 2, enumerable: true });
  Object.defineProperty(window.WebSocket, 'CLOSED', { value: 3, enumerable: true });

  const disconnectHMR = () => {
    if (hmrSocket && (hmrSocket.readyState === 0 || hmrSocket.readyState === 1)) {
      log('Disconnecting HMR websocket (tab hidden) - saving costs ✅');
      hmrSocket.close(1000, 'Tab hidden');
      hmrSocket = null;
    }
    shouldBlockReconnect = true;
  };

  const reconnectHMR = () => {
    shouldBlockReconnect = false;

    // Clear any blocked timers
    blockedTimers.forEach(timerId => {
      try {
        clearTimeout(timerId);
        clearInterval(timerId);
      } catch (e) {}
    });
    blockedTimers.clear();

    // If HMR was disconnected, reload to reconnect
    if (!hmrSocket || hmrSocket.readyState === 3) {
      log('Reloading page to restore HMR connection');
      setTimeout(() => window.location.reload(), 100);
    }
  };

  // Handle tab visibility changes
  document.addEventListener('visibilitychange', () => {
    isTabVisible = !document.hidden;

    if (document.hidden) {
      log('Tab hidden - will disconnect HMR in ' + DISCONNECT_DELAY + 'ms');

      disconnectTimer = OriginalSetTimeout(() => {
        if (document.hidden) {
          disconnectHMR();
        }
      }, DISCONNECT_DELAY);

    } else {
      log('Tab visible - allowing HMR reconnection');

      // Clear disconnect timer if tab becomes visible again
      if (disconnectTimer) {
        clearTimeout(disconnectTimer);
        disconnectTimer = null;
      }

      OriginalSetTimeout(() => {
        reconnectHMR();
      }, 100);
    }
  });

  // Handle page freeze events
  window.addEventListener('freeze', () => {
    log('Page freeze detected - disconnecting HMR immediately');
    disconnectHMR();
  }, { capture: true });

  window.addEventListener('resume', () => {
    log('Page resume detected - allowing HMR reconnection');
    OriginalSetTimeout(() => {
      reconnectHMR();
    }, 100);
  }, { capture: true });

  log('HMR Visibility Manager initialized - will block reconnection attempts when tab is hidden');
})();
          `,
          injectTo: 'head-prepend',
        },
      ]
    },
  }
}

Sandbox statuses

Blaxel sandboxes go from standby to active in under 25 milliseconds, and scale back down to standby after a few seconds of inactivity, maintaining their previous state after scaling down.
  • In STANDBY mode: You are not charged for CPU/memory while a sandbox is in standby mode. However, you are charged for the storage of the snapshot and/or the volumes.
  • In ACTIVE mode: You are charged for CPU/memory and storage while a sandbox is in active mode. Sandboxes automatically return to standby mode after 1 second of inactivity.
The scale-to-zero functionality is based on network activity. When your connection to the sandbox closes, Blaxel automatically creates a snapshot of the entire state (including the complete file system in memory, preserving both files and running processes) and transitions to standby mode within approximately 5 seconds. Any running processes are included in this snapshot and will be instantly restored when you reconnect to the sandbox. The possible sandbox statuses are:
  • UPLOADING: A new sandbox version has just been uploaded; the build has not started yet.
  • BUILDING: A new sandbox version has been uploaded and the build is in progress.
  • DEPLOYING: The sandbox deployment is in progress.
  • DEPLOYED: The sandbox is ready to use.
  • FAILED: An error occurred during the build or deployment of the sandbox.
  • TERMINATED: A TTL was set for the sandbox; it has been deleted and will be removed from the API/UI soon.
  • DELETING: A deletion request has been triggered and the deletion is in progress.
UPLOADING/BUILDING statuses only appear when using bl deploy from a sandbox template folder.
See tutorials and examples: Or explore the Sandbox API reference:

Sandbox API

Access the your sandbox with an HTTP REST API.