Running in production
A checklist for taking Lightcone from prototype to production, covering pinning, idempotency, resumable streams, error handling, and cost control.
Prototypes tolerate a lost stream or a double-run. Production doesn’t. This guide covers the things that separate a demo from a service you can page someone about.
Pin your model version
Section titled “Pin your model version”Use a fully versioned model ID:
client.agent.tasks.start( instruction="...", kind="browser", model="tzafon.northstar-cua-fast-1.6", # pinned)await client.agent.tasks.start({ instruction: "...", kind: "browser", model: "tzafon.northstar-cua-fast-1.6", // pinned});Unversioned aliases move under you: behavior, step counts, and costs shift when the alias is repointed to a newer model. Pin a version in production and upgrade deliberately, with your own evals in between.
Make retries safe with idempotency keys
Section titled “Make retries safe with idempotency keys”Networks fail mid-request. If you retry a task start without protection, you run the task twice: twice the cost, and possibly twice the side effects (two orders placed, two emails sent).
Pass an idempotency_key on task creation. A retry with the same key returns the same task with reused: true instead of starting a new one:
task = client.agent.tasks.start( instruction="Submit the expense report for invoice INV-4821", kind="browser", idempotency_key="expense-INV-4821",)if task.reused: print(f"Already running as {task.task_id}")const task = await client.agent.tasks.start({ instruction: "Submit the expense report for invoice INV-4821", kind: "browser", idempotency_key: "expense-INV-4821",});if (task.reused) { console.log(`Already running as ${task.task_id}`);}Derive the key from your own job ID so retries at any layer (your queue, your HTTP client, the SDK) all deduplicate to one task.
Resume streams after disconnects
Section titled “Resume streams after disconnects”SSE connections drop when load balancers rotate, laptops sleep, or pods restart. The task keeps running; you just need to reattach without losing events.
Reconnect to GET /agent/tasks/{id}/stream with the standard Last-Event-ID header (SSE clients send it automatically), or pass ?from=<seq> explicitly. The server replays buffered events from that point:
import httpx
with httpx.stream( "GET", f"https://api.tzafon.ai/agent/tasks/{task_id}/stream", headers={ "Authorization": f"Bearer {api_key}", "Last-Event-ID": str(last_seq), # resume where you left off }, timeout=None,) as response: for line in response.iter_lines(): ...// EventSource sends Last-Event-ID automatically on reconnect.// With fetch-based clients, pass ?from=<seq> explicitly:const res = await fetch( `https://api.tzafon.ai/agent/tasks/${taskId}/stream?from=${lastSeq}`, { headers: { Authorization: `Bearer ${apiKey}` } },);If you reconnect after the buffer has evicted your position, you receive a replay_gap event, meaning some events are gone for good. Re-sync from retrieve_status and continue from live events.
Streams remain available for roughly 10 minutes after a task finishes, so a consumer that reconnects late can still collect the terminal event.
Handle errors by taxonomy, not by string
Section titled “Handle errors by taxonomy, not by string”A terminal failed event carries error_code and retryable:
retryable: true(e.g.session_expired, upstream 503): retry with exponential backoff, ideally on a fresh computer.retryable: false: don’t loop. Route to human review with the last screenshot and the task ID.
On the HTTP side:
| Status | Meaning | What to do |
|---|---|---|
402 | Out of credits | Top up at lightcone.ai/dashboard/usage. Alert, do not retry |
429 | Concurrent-session limit reached | Queue and retry with backoff, or raise your limit |
4xx other | Request problem | Fix the request; retrying won’t help |
5xx | Server problem | Retry with backoff (the SDKs do this automatically) |
Error responses use RFC 7807 application/problem+json bodies, so you can branch on the type field rather than parsing message strings.
Session hygiene
Section titled “Session hygiene”Leaked computers are the most common source of surprise bills.
- Always delete computers when done. Use context managers (Python) or
try/finally(TypeScript), and set tightmax_lifetime_seconds/inactivity_timeout_secondsas a backstop. - Keepalive during long think-time. Any action resets the idle clock, but gaps between your process’s calls do not. If your code spends minutes deciding between actions (waiting on a model, a human, or a queue), send
keep_alive()periodically or the computer will be reaped under you.
computer.keep_alive() # call periodically during long gaps between actionsawait client.computers.keepalive(computer.id!);Warm sessions for latency-sensitive services
Section titled “Warm sessions for latency-sensitive services”Creating and signing in to a fresh computer per request is slow. For interactive services, keep one authenticated computer per worker and adopt it on startup:
def get_computer(client, env_id): # Adopt a live computer if one exists for c in client.computers.list(): if c.status == "running": try: # Verify liveness with a cheap real action # a stale session can still return screenshots client.computers.navigate(c.id, url="https://app.example.com/ping") return c except Exception: client.computers.delete(c.id) # Otherwise recreate from the authenticated snapshot return client.computers.create( kind="browser", environment_id=env_id, persistent=True )async function getComputer(client: Lightcone, envId: string) { for await (const c of client.computers.list()) { if (c.status !== "running") continue; try { // Verify liveness with a cheap real action, not just a screenshot await client.computers.navigate(c.id!, { url: "https://app.example.com/ping" }); return c; } catch { await client.computers.delete(c.id!); } } return client.computers.create({ kind: "browser", environment_id: envId, persistent: true, });}The verification action matters: check liveness with something that exercises the session (a navigation, a real click), not just a screenshot. A session whose login has expired will happily screenshot the login page. On any failure, delete and recreate from your persistent environment_id (see Logins and sessions).
Set timeouts at every layer
Section titled “Set timeouts at every layer”- Wrap SDK calls in your own timeouts. Individual actions default to ~20 seconds server-side, but model calls in tasks and the Responses API can take longer. Do not let one hung call stall a worker forever.
- Set
max_duration_secondsandmax_stepson every task. Never ship a task request without both; they’re your only hard ceilings on wall-clock time and cost.
client.agent.tasks.start( instruction="...", kind="browser", model="tzafon.northstar-cua-fast-1.6", max_steps=40, max_duration_seconds=600, idempotency_key=job_id,)await client.agent.tasks.start({ instruction: "...", kind: "browser", model: "tzafon.northstar-cua-fast-1.6", max_steps: 40, max_duration_seconds: 600, idempotency_key: jobId,});Watch costs
Section titled “Watch costs”max_stepscaps runaway agents. An agent stuck in a loop burns one model call per step until something stops it.- Each computer-use request carries a fixed overhead of roughly 2k tokens (screenshot and context framing) before your instruction, so many tiny tasks cost more than one well-scoped task.
- Idle computers bill until they time out or are deleted; see session hygiene above.
See Pricing for rates and how metering works.
Production checklist
Section titled “Production checklist”- Model pinned to a full version (
tzafon.northstar-cua-fast-1.6), upgrades gated on your own evals idempotency_keyon every task start, derived from your job ID- Stream consumer resumes with
Last-Event-ID/?from=and re-syncs from status onreplay_gap failedevents branched onretryable; 402 alerts, 429 backs off- Computers deleted in
finallyblocks, tight timeouts set, keepalive during long gaps - Warm sessions verified with a real action, recreated from
environment_idon failure - Client-side timeouts on every SDK call;
max_stepsandmax_duration_secondson every task - Cost dashboards watched at lightcone.ai/dashboard/usage
See also
Section titled “See also”- Logins and sessions: persistent authenticated sessions
- Tasks: events, streaming, and configuration
- Best practices: reliability patterns for computer control
- Pricing: rates and metering