--- title: Best Practices | Lightcone description: Patterns for reliable, efficient, and cost-effective Lightcone automation. --- These patterns will help you build reliable automations that handle real-world conditions. They’re distilled from common issues and proven workflows. ## Session management ### Use context managers (Python) Always use `with` blocks for sessions. They guarantee cleanup even if your code raises an exception: ``` # Good — session always terminates with client.computer.create(kind="browser") as computer: computer.navigate("https://example.com") # session terminates when block exits, even on error # Avoid — session leaks if an error occurs before delete() session = client.computers.create(kind="browser") client.computers.navigate(session.id, url="https://example.com") client.computers.delete(session.id) # never reached if navigate() throws ``` ``` // Always use try/finally for cleanup const computer = await client.computers.create({ kind: "browser" }); const id = computer.id!; try { await client.computers.navigate(id, { url: "https://example.com" }); } finally { await client.computers.delete(id); } ``` ### Set appropriate timeouts Choose timeouts based on your task, not defaults: ``` # Quick scrape — short timeout to avoid paying for idle sessions with client.computer.create( kind="browser", timeout_seconds=120, inactivity_timeout_seconds=30, ) as computer: # ... # Long-running agent — generous timeout with keepalive disabled with client.computer.create( kind="browser", timeout_seconds=3600, auto_kill=False, ) as computer: # ... ``` ``` // Quick scrape const computer = await client.computers.create({ kind: "browser", timeout_seconds: 120, inactivity_timeout_seconds: 30, }); // Long-running agent const computer = await client.computers.create({ kind: "browser", timeout_seconds: 3600, auto_kill: false, }); ``` ### Reuse sessions with persistence Don’t create a new session for every run when you can reuse one. Persistent sessions save cookies, local storage, and browser cache: ``` # First run: create and save with client.computer.create(kind="browser", persistent=True) as computer: computer.navigate("https://app.example.com/login") # ... log in ... session_id = computer.id # Save session_id to a file or database # Subsequent runs: restore with client.computer.create( kind="browser", environment_id=session_id, ) as computer: computer.navigate("https://app.example.com/dashboard") # Already logged in — no need to re-authenticate ``` ``` // First run const session = await client.computers.create({ kind: "browser", persistent: true, }); // ... log in, then save session.id somewhere ... await client.computers.delete(session.id!); // Subsequent runs const restored = await client.computers.create({ kind: "browser", environment_id: savedSessionId, }); ``` ## Screenshots and coordinates ### Always screenshot before clicking Page layouts shift between loads. Never hardcode coordinates — always take a fresh screenshot to determine the current positions: ``` computer.navigate("https://example.com") computer.wait(2) # Screenshot to see the current layout result = computer.screenshot() url = computer.get_screenshot_url(result) print(f"Check layout: {url}") # Use coordinates from the screenshot, not from a previous run computer.click(500, 300) ``` ``` await client.computers.navigate(id, { url: "https://example.com" }); await new Promise((r) => setTimeout(r, 2000)); const result = await client.computers.screenshot(id); console.log("Check layout:", result.result?.screenshot_url); await client.computers.click(id, { x: 500, y: 300 }); ``` ### Prefer selectors over coordinates when possible For more reliable element targeting, use the [Playwright integration](/integrations/playwright/index.md) with CSS selectors, or use the [Responses API](/guides/responses-api/index.md) to let a vision model find click targets automatically. ### Verify actions with follow-up screenshots After a critical action (clicking a button, submitting a form), take another screenshot to confirm it worked: ``` computer.click(400, 300) # Click "Submit" computer.wait(2) result = computer.screenshot() url = computer.get_screenshot_url(result) # Verify: did a confirmation page appear? ``` ## Waits and timing ### Wait after navigation Pages need time to load. Always add a wait after `navigate()`: ``` computer.navigate("https://example.com") computer.wait(2) # Wait for page load # For JavaScript-heavy SPAs, wait longer computer.navigate("https://app.example.com/dashboard") computer.wait(5) ``` ``` await client.computers.navigate(id, { url: "https://example.com" }); await new Promise((r) => setTimeout(r, 2000)); // For SPAs await client.computers.navigate(id, { url: "https://app.example.com/dashboard" }); await new Promise((r) => setTimeout(r, 5000)); ``` ### Wait between rapid actions Browsers need time to process each action. Add short waits between interactions that modify the page: ``` computer.click(400, 300) # Click a dropdown computer.wait(1) # Wait for dropdown to open computer.click(400, 380) # Click an option computer.wait(1) # Wait for selection to apply ``` ### Use batch actions for speed When you have multiple actions that don’t need screenshots between them, use batch to execute them in a single request: ``` client.computers.batch(session.id, actions=[ {"type": "click", "x": 400, "y": 300}, {"type": "type", "text": "search query"}, {"type": "hotkey", "keys": ["Enter"]}, ]) ``` ``` await client.computers.batch(id, { actions: [ { type: "click", x: 400, y: 300 }, { type: "type", text: "search query" }, { type: "hotkey", keys: ["Enter"] }, ], }); ``` ## Error handling ### Catch session errors gracefully Sessions can time out, get terminated, or encounter network issues. Wrap session work in error handling: ``` import tzafon try: with client.computer.create(kind="browser") as computer: computer.navigate("https://example.com") computer.wait(2) result = computer.screenshot() except tzafon.APITimeoutError: print("Request timed out — try increasing the SDK timeout") except tzafon.APIStatusError as e: if e.status_code == 404: print("Session not found — it may have timed out") elif e.status_code == 429: print("Rate limited — reduce concurrency or add delays") else: raise ``` ``` import Lightcone from "@tzafon/lightcone"; try { const computer = await client.computers.create({ kind: "browser" }); // ... } catch (e) { if (e instanceof Lightcone.APITimeoutError) { console.error("Request timed out"); } else if (e instanceof Lightcone.APIStatusError) { console.error(`API error ${e.status}: ${e.message}`); } else { throw e; } } ``` ### Don’t retry blindly The SDKs already retry on transient errors (429, 5xx) with exponential backoff — default 2 retries. Adding your own retry loop on top can cause excessive requests. Instead, increase the SDK’s retry limit if needed: ``` client = Lightcone(max_retries=5) ``` ``` const client = new Lightcone({ maxRetries: 5 }); ``` ## Agent Tasks ### Write specific instructions Vague instructions lead to unpredictable results. Be explicit about what “done” looks like: ``` # Vague — agent may give up or take wrong path client.agent.tasks.start( instruction="Find cheap flights", kind="browser", ) # Specific — agent knows exactly what to do and when it's done client.agent.tasks.start( instruction=( "Go to google.com/travel/flights. " "Search for round-trip flights from SFO to JFK, " "departing March 15 and returning March 22. " "Sort by price (lowest first). " "You're done when the results page shows sorted flights." ), kind="browser", max_steps=30, ) ``` ### Set max\_steps to prevent runaway agents Always set `max_steps` to a reasonable limit for your task: | Task type | Suggested max\_steps | | -------------------------------- | -------------------- | | Simple navigation and screenshot | 5–10 | | Form filling | 10–20 | | Multi-page workflow | 20–40 | | Complex research task | 40–100 | ### Use streaming to monitor progress Streaming lets you see what the agent is doing in real time and intervene if it goes off track: ``` for event in client.agent.tasks.start_stream( instruction="...", kind="browser", max_steps=20, ): if hasattr(event, "screenshot_url"): print(f"Step screenshot: {event.screenshot_url}") elif hasattr(event, "status"): print(f"Status: {event.status}") ``` ``` const stream = await client.agent.tasks.startStream({ instruction: "...", kind: "browser", max_steps: 20, }); for await (const event of stream) { console.log(event); } ``` ## Anti-detection ### Use the advanced proxy for sensitive sites Sites with bot detection (e-commerce, social media, banking) need the advanced proxy: ``` with client.computer.create( kind="browser", use_advanced_proxy=True, ) as computer: computer.navigate("https://protected-site.com") ``` ### Add human-like delays Even with the proxy, unnaturally fast interactions can trigger bot detection. Add realistic pauses: ``` import random computer.click(400, 300) computer.wait(random.uniform(0.5, 1.5)) # Human-like pause computer.type("search query") computer.wait(random.uniform(0.3, 0.8)) computer.hotkey("enter") ``` ### Respect robots.txt and terms of service Lightcone gives you the power to automate any website. Use that power responsibly — check a site’s `robots.txt` and terms of service before automating. ## See also - [**Computers**](/guides/computers/index.md) — session lifecycle, actions, and configuration - [**Troubleshooting**](/guides/troubleshooting/index.md) — solutions for common issues - [**Agent Tasks**](/guides/agent-tasks/index.md) — autonomous AI agents - [**How Lightcone works**](/guides/how-lightcone-works/index.md) — architecture and session model