Skip to main content
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 manage standby mode. 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',
        },
      ]
    },
  }
}