Skip to main content
Machines are the live runtime unit in Nullspace. They are created from a template and expose command, filesystem, desktop, PTY, lifecycle, and port URL helpers.

Create

from nullspace import Machine

with Machine.create(
    template="base",
    vcpus=2,
    memory_mb=512,
    timeout=300,
    on_timeout="pause",
    auto_resume=True,
    envs={"APP_ENV": "dev"},
    metadata={"user_id": "user_123"},
    cwd="/workspace",
    internet_access=True,
    network={"mask_request_host": "localhost:${PORT}"},
) as machine:
    print(machine.id)
Use on_timeout="pause", auto_resume=True when preview URL traffic should wake a paused machine before forwarding the original request. auto_resume=True is only valid with pause/hibernate timeout behavior. Use a template warm pool for ready capacity before a burst:
machine = Machine.create(
    template="team/fastapi-app:stable",
    warm_pool="twp_fastapi_small",
    warm_pool_mode="prefer",
    warm_pool_wait_ms=1500,
)
print(machine.get_info().warm_pool_checkout)
warm_pool_mode="require" fails with warm_pool_unavailable when no matching ready instance is available. Snapshot restore, resume, fork, hibernate, and pause do not use template warm-pool checkout.

Egress controls

Restrict a machine’s outbound network access with a typed egress policy. You can set it at create time and change it on a running machine without recreate.
from nullspace import Machine, NetworkEgressPolicy, EgressRateLimit

# Block all outbound traffic at create time (fail-closed).
with Machine.create(
    template="base",
    network={"egress": NetworkEgressPolicy.deny_all().to_dict()},
) as machine:
    # Later, broaden to an allowlist with a best-effort rate limit.
    machine.update_egress(
        NetworkEgressPolicy.custom(
            allow_cidrs=["1.1.1.1/32"],
            rate_limit=EgressRateLimit(kbit_per_sec=1000, burst=64),
        )
    )
Modes are allow_all, deny_all, and custom. custom carries IPv4 allow_cidrs/deny_cidrs and an optional outbound rate_limit (set one of kbit_per_sec or pps, with an optional burst). The policy is IPv4-only and does not do domain/DNS filtering. Tightening (e.g. moving to deny_all) fails closed; broadening only succeeds once the new rules apply. The policy survives hibernate/resume. The rate limit is best-effort shaping, not a hard quota. The legacy internet_access, network.deny_out, and network.allow_out fields still work but cannot express a rate limit; internet_access=False is fail-closed and cannot be re-opened by a stray allow rule.

Inspect and connect

machines = Machine.list(limit=20)
for item in machines.items:
    print(item.id, item.status, item.template)

while machines.has_next:
    for item in machines.next_items():
        print(item.id)

machine = Machine.connect("mch_...")
print(machine.get_info().status)
Machine.connect(id) is an explicit reconnect operation. If id currently points at a paused snapshot, connect() resumes it and returns the running machine execution. Machine.get_info_by_id(id) is read-only and does not wake a paused machine. Pass fields=["id", "status"] to Machine.list() or Machine.get_info_by_id() for compact raw dictionaries instead of full model objects. This is useful for dashboards and polling loops that only need a few attributes.

Preview URLs and timeout

machine.set_timeout(120, timeout_action="hibernate")
host_info = machine.get_host_info(8080)
print(host_info.url, host_info.websocket_url)
print(machine.get_url(8080))
preview = machine.create_signed_preview_url(8080, expires_in_seconds=900)
readiness = machine.wait_for_preview(8080, timeout_secs=30)
print(readiness.ready, preview.grant.id)
print(machine.get_metrics())
For machines created with on_timeout="pause", auto_resume=True, HTTP and websocket traffic to those preview URLs wakes the paused machine, waits for the bounded resume window, and then forwards to the resumed execution. If auto-resume is disabled or the wake does not complete in time, the preview URL returns 503 Service Unavailable with Retry-After: 5. Use machine.create_preview_proxy_target(8080) when your application proxy should forward to Nullspace with x-nullspace-preview-proxy-token instead of putting signed edge tokens in browser-visible URLs. Use redact_preview_url(...) or redact_preview_token(...) before printing preview credentials to logs or terminal output.

Lifecycle events

Use machine.stream_events() to tail lifecycle events related to one machine without polling:
from nullspace import Machine

machine = Machine.connect("mch_123")
try:
    for event in machine.stream_events():
        print(event.operation, event.status, event.snapshot_id)
finally:
    machine.close()
async for event in machine.stream_events(live_only=True):
    print(event.id, event.operation)
The iterator uses GET /v1/machines/{id}/lifecycle/ws, reconnects automatically, and resumes with after_event_id from the last yielded event. Use live_only=True to skip the initial replay while still keeping reconnects gap-resistant after the first event. Shared volume data is external durable storage. Resume and fork remount shared volume attachments, but do not make VM memory or mutable rootfs state portable across incompatible runtime hosts.

One-shot runs

result = Machine.run_once(
    "python3",
    ["-c", "print('hello')"],
    template="base",
    timeout=60,
)
print(result.stdout)
Use run_once for simple tasks where the SDK should create a machine, run the code, and clean up without exposing a long-lived handle.

Async API

from nullspace import AsyncMachine

async with await AsyncMachine.create(template="base", timeout=300) as machine:
    result = await machine.commands.run("echo async", shell=True)
    print(result.stdout)

    async for event in machine.stream_events(live_only=True):
        print(event.operation, event.status)
        break
Async helpers follow the same pagination and fields behavior as the synchronous SDK. Concepts: Create, Destroy, Fork. API reference: createMachine, getMachine, getHost, setTimeout.