Docs

Configure and manage sandboxes

Manage the lifecycle of a running sandbox: pause it to stop compute charges, resume it, resize it, copy files in and out, watch what it is doing, and delete it when you are finished.

Every example on this page assumes you have an initialised apiClient and a sandboxId. See the quickstart if you do not.

Pause and resume a sandbox

Pausing stops compute billing and ends any running processes and terminal sessions. Configuration is kept, and data on an attached volume survives.

Pausing deletes ephemeral data

Before pausing, copy files that you need to keep to an attached volume. Anything written only to ephemeral storage is lost when the sandbox pauses. See create a sandbox with persistent storage.

Northflank has dedicated pause and resume endpoints, which are the calls to reach for:

await apiClient.pause.service({
  parameters: {
    projectId: 'your-project-id',
    serviceId: sandboxId,
  },
});

Scaling to zero instances has the same billing effect and is useful when you are already adjusting scale, but pause states the intent and is what the dashboard and CLI use.

Resume

Resume takes a body, which can be empty. Pass overrides in it if you want to change settings as the sandbox comes back up, then wait for it to report COMPLETED:

// Resuming a sandbox that is not paused returns an error, so check the
// status first if you are unsure of its state.
await apiClient.resume.service({
  parameters: {
    projectId: 'your-project-id',
    serviceId: sandboxId,
  },
  data: {},
});

async function waitForReady() {
  while (true) {
    const svc = await apiClient.get.service({
      parameters: {
        projectId: 'your-project-id',
        serviceId: sandboxId,
      },
    });

    const status = svc.data?.status?.deployment?.status;
    if (status === 'COMPLETED') return;
    if (status === 'FAILED') throw new Error('Sandbox failed to resume');

    await new Promise((r) => setTimeout(r, 1000));
  }
}

await waitForReady();

Files on an attached volume come back exactly as they were. Ephemeral storage starts empty.

Scale resources

Change the CPU and memory available to a sandbox by scaling it to a different deployment plan. The scale endpoint takes deploymentPlan, instances, and storage, so you can change allocation and instance count in one call.

await apiClient.scale.service({
  parameters: {
    projectId: 'your-project-id',
    serviceId: sandboxId,
  },
  data: {
    // The sandbox restarts to pick up the new plan, so expect a brief
    // interruption rather than a live resize.
    deploymentPlan: 'nf-compute-400',
  },
});

Scaling is the call that changes a plan. The update deployment endpoint changes the deployment source, such as the image or Docker configuration, and does not accept billing fields.

See resource plans.

Copy files to and from a sandbox

Copy files and directories between your machine and a running sandbox. Both SDKs stream the transfer over the same exec connection used to run commands, so the sandbox image needs tar on its PATH.

await apiClient.upload.service.files(
  {
    projectId: 'your-project-id',
    serviceId: sandboxId,
  },
  {
    localPath: './script.py',
    remotePath: '/workspace',
  }
);

await apiClient.download.service.files(
  {
    projectId: 'your-project-id',
    serviceId: sandboxId,
  },
  {
    localPath: './results',
    remotePath: '/workspace/results',
  }
);

Directories are copied recursively. A missing local path on upload, or a missing remote path on download, fails before any data is transferred. Both calls take a timeout in seconds, which defaults to 300 in Python.

Pulling large artefacts is usually faster from inside the sandbox. Exec a curl or git clone against the source rather than routing the bytes through your own machine.

See copy files for the CLI equivalent.

View sandbox logs

Stream output from a running sandbox.

const parameters = {
  projectId: 'your-project-id',
  serviceId: sandboxId,
};
const options = { lineLimit: 100 };

const logsClient = await apiClient.get.service.logTail({ parameters, options });

logsClient.on('logs-received', (logLines) => {
  logLines.forEach((l) => console.log(l.log));
});

logsClient.on('error', (error) => console.log('error', error));

// Lines arrive batched, either at 32.7kb or once per second.
// Call await logsClient.stop() to close the connection.
await logsClient.start();

Fetch a recent range

The Python SDK also fetches a fixed range without holding a connection open. It returns LogLine objects with ts, log, and container_id:

lines = client.logs.fetch_service_logs(
    project_id="your-project-id",
    service_id=sandbox_id,
    line_limit=100,
    direction="backward",
    # text_includes="error" filters server-side, which is much cheaper
    # than pulling everything and filtering locally.
)

for line in lines:
    print(line.ts, line.log)

See log tailing for the full set of options and events.

Monitor resource usage

Check CPU, memory, and network usage for a sandbox with get.service.metrics.

const metrics = await apiClient.get.service.metrics({
  parameters: {
    projectId: 'your-project-id',
    serviceId: sandboxId,
  },
  options: {
    // ISO timestamp plus a duration in seconds. This covers the last hour.
    startTime: new Date(Date.now() - 3600_000).toISOString(),
    duration: 3600,
  },
});

console.log(metrics.data);

See metrics for the response shape, and observability on Northflank for dashboards and alerting.

Delete a sandbox

Deletion cannot be undone

Deleting a sandbox destroys the service. Deleting its volume destroys the data on it permanently, with no way to recover it. Copy out anything you need first.

Delete the service to remove the sandbox.

await apiClient.delete.service({
  parameters: {
    projectId: 'your-project-id',
    serviceId: sandboxId,
  },
});

Delete an attached volume

A volume is a separate resource and outlives the sandbox it was attached to. A volume cannot be deleted while it is still attached, so detach it first, delete the service, then delete the volume.

await apiClient.detach.volume({
  parameters: {
    projectId: 'your-project-id',
    volumeId: volumeId,
  },
  data: {
    nfObject: { id: sandboxId, type: 'service' },
  },
});

await apiClient.delete.service({
  parameters: {
    projectId: 'your-project-id',
    serviceId: sandboxId,
  },
});

// Detaching settles asynchronously, so this can return 409 while it is
// still in progress. Wait a few seconds and retry. 404 or 410 means the
// volume is already gone.
await apiClient.delete.volume({
  parameters: {
    projectId: 'your-project-id',
    volumeId: volumeId,
  },
});

If you only want to stop paying for compute, pause the sandbox instead of deleting it.

© 2026 Northflank Ltd. All rights reserved.

northflank.com / Terms / Privacy / feedback@northflank.com