Tutorial: Reconnect to a running sandbox

This tutorial demonstrates that a Cortex Sandbox is a durable container — it keeps running even after the Python process that created it exits. When your driver restarts (or crashes), you can reconnect to the sandbox by name, inspect its state, and wait for its job to complete.

When you finish, you will have a pattern for building agent systems that survive driver restarts without losing work.

Time to complete: about 10 minutes.

Prerequisites

Step 1: Start a long-running job

The following script creates a sandbox with a long-running command and exits without waiting for the job to finish. The sandbox continues running after the driver exits.

Save this as start_job.py.

# start_job.py — start a job and exit; the sandbox keeps running
from snowflake.sandbox import Sandbox

CONNECTION = "<your_connection_name>"

JOB = """
import time, datetime, sys

print("Job started:", datetime.datetime.now().isoformat(), flush=True)
for i in range(1, 7):
    time.sleep(10)
    print(f"Step {i}/6 complete — {datetime.datetime.now().isoformat()}", flush=True)
print("Job finished.", flush=True)
"""

sb = Sandbox.create(
    connection=CONNECTION,
    command=["python", "-c", JOB],
    name="long-running-demo",
)
print(f"Job started in sandbox: {sb.id}")
print("Driver exiting — sandbox keeps running. Reconnect with the name above.")

Run it:

python start_job.py

Expected output:

Job started in sandbox: cntr_9f2a1b...
Driver exiting — sandbox keeps running. Reconnect with the name above.

The script exits immediately. The sandbox and its job are still running in Snowflake.

Step 2: List running sandboxes

If you already know the sandbox’s name from Step 1, skip to Step 3. If you need to discover it, use Sandbox.list().

Save this as list_sandboxes.py:

# list_sandboxes.py — find running sandboxes
from snowflake.sandbox import Sandbox, using

CONNECTION = "<your_connection_name>"

# Sandbox.list() also accepts connection= directly; using() is the convenience
# when several calls in a row should share one connection.
with using(CONNECTION):
    sandboxes = Sandbox.list(status="ready")

if not sandboxes:
    print("No running sandboxes found.")
else:
    print(f"{'NAME':<24} {'ID':<22} {'Status'}")
    print("-" * 58)
    for sb in sandboxes:
        print(f"{sb.name or '-':<24} {sb.id:<22} {sb.status}")

Expected output:

NAME                     ID                     Status
----------------------------------------------------------
long-running-demo        cntr_9f2a1b...         ready

Step 3: Reconnect by name and wait for the job

Use Sandbox.connect(name) to reattach to the running sandbox. Then read logs that have accumulated since the job started, and wait for it to finish.

Save this as reconnect.py. SANDBOX_NAME is the name you passed in Step 1.

# reconnect.py — reattach to a running sandbox by its name
from snowflake.sandbox import Sandbox, using

CONNECTION   = "<your_connection_name>"
SANDBOX_NAME = "long-running-demo"

print(f"Reconnecting to {SANDBOX_NAME}...")

# Only the attach needs the named connection: the handle keeps the connection it
# was built with, so the calls below it do not need to stay inside the block.
with using(CONNECTION):
    sb = Sandbox.connect(SANDBOX_NAME)

# Re-read the container's state from the server
status = sb.refresh()
print(f"Status: {status}")

# Read logs accumulated so far
print("\n--- Logs so far ---")
logs = sb.logs(tail=20)
print(logs or "(no output yet)")

# Wait for the job to complete
print("Waiting for job to finish...")
result = sb.wait()
print(f"\nJob completed: status={result.status}, exit_code={result.exit_code}")

# Read the final logs
print("\n--- Final logs ---")
print(sb.logs(tail=20))

Run it while the job is still running:

python reconnect.py

Expected output:

Reconnecting to long-running-demo...
Status: ready

--- Logs so far ---
Job started: 2026-08-18T10:32:14.001234
Step 1/6 complete — 2026-08-18T10:32:24.112345
Step 2/6 complete — 2026-08-18T10:32:34.223456

Waiting for job to finish...

Job completed: status=succeeded, exit_code=0

--- Final logs ---
Job started: 2026-08-18T10:32:14.001234
Step 1/6 complete — 2026-08-18T10:32:24.112345
Step 2/6 complete — 2026-08-18T10:32:34.223456
Step 3/6 complete — 2026-08-18T10:32:44.334567
Step 4/6 complete — 2026-08-18T10:32:54.445678
Step 5/6 complete — 2026-08-18T10:33:04.556789
Step 6/6 complete — 2026-08-18T10:33:14.667890
Job finished.
__SANDBOX_RESULT__eyJzdGF0dXMiOiAic3VjY2VlZGVkIiwgLi4ufQ==

The trailing __SANDBOX_RESULT__ line is printed by the wrapper the SDK puts around a command= entrypoint so wait() can recover the run’s exit code. It is part of the container’s stdout, so it shows up in logs() alongside your own output.

The logs from before the reconnect are fully available — no output is lost when the driver restarts.

Step 4: Clean up

Every other tutorial wraps its sandbox in with Sandbox.create(...) as sb:, which terminates the container when the block exits. This one deliberately does not: the sandbox has to outlive the process that created it, which is the whole point. That makes cleaning up your job rather than the context manager’s.

# cleanup.py — terminate the sandbox from Step 1
from snowflake.sandbox import Sandbox, using

CONNECTION   = "<your_connection_name>"
SANDBOX_NAME = "long-running-demo"

with using(CONNECTION):
    sb = Sandbox.connect(SANDBOX_NAME)

sb.terminate()
print(f"Terminated {SANDBOX_NAME}")

Important

Nothing deletes a sandbox for you. There is no TTL, and the platform suspends an idle sandbox — so it stops consuming compute — but never removes it. A suspended sandbox still counts against your quota of 100 live sandboxes per identity, and a create past that cap is refused. If you work through several tutorials without terminating, you will eventually be unable to create sandboxes at all until you clean up. See Limitations and FAQ.

To find sandboxes you have left behind, list them and terminate what you no longer need:

from snowflake.sandbox import Sandbox, using

with using("<your_connection_name>"):
    for info in Sandbox.list(status="ready"):
        print(info.name or "-", info.id, info.status)

What this demonstrates

  • Sandboxes outlive their creator. The container keeps running after the Python process that created it exits. This is fundamental to building reliable agent systems where the driver can restart without losing work.
  • Reconnect is cheap. Sandbox.connect(name) performs a single lookup request — it does not re-initialize the container, and the running process is undisturbed.
  • Reconnecting is not the same as the container never having restarted. A sandbox that had been suspended always comes back as a fresh container, and Snowflake may additionally recreate an unhealthy one. Either way, in-container /tmp and /snowflake/stages are wiped and every process that was running is gone, so anything your job wrote there is lost even though connect() succeeds and the name still resolves. Writes to the persistent rootfs (/var/tmp, /var/log, /home, /root) do come back. Read sb.generation and compare it across calls: a change means a restart happened and you must re-stage whatever you cannot recompute. Keep durable state in a Snowflake table or a stage, not on the container’s filesystem.
  • Logs are durable. The container’s output survives every driver disconnect and reconnect — sb.logs() reads it back whatever happened to the driver. Pass tail= to read only the last N lines; the server clamps that to 10,000.
  • Sandbox.list() is the management surface. If you lose the name, Sandbox.list(status="ready") discovers every live sandbox visible to your session.

Going further

  • Use Sandbox.list() in your orchestrator’s startup path to detect and reconnect to sandboxes left over from a previous run.
  • Persist the sandbox name alongside the job metadata in a Snowflake table so you can reconnect from any host in your fleet.
  • Set idle_suspend= on Sandbox.create() to control how long the sandbox may sit idle before Snowflake suspends it. Suspension and resumption are handled by the platform, so you do not have to manage them — but you can: sb.suspend() banks state immediately rather than at the end of the idle window, and sb.resume() pays the cold start up front instead of inside your next exec().
  • For an interactive, long-lived session, drive the sandbox with sb.send() instead of a one-shot command. See sb.send() in the SDK reference.