How to Run the Temporal Dev Server in a Cloud Sandbox
Updated October 27, 2026 .md
Short answer: Install the Temporal CLI, run temporal server start-dev with --db-filename under systemd, and host the Web UI on port 8233 with sbx host --localhost. On a small sandbox the server was ready in 1.5 s and a one-activity workflow took about 100 ms. History survived stop, resume and a cold boot.
At a glance#
Everything below ran on production on October 27, 2026: Temporal CLI 1.9.1 (Server 1.32.0, UI 2.54.1) and the Python SDK temporalio 1.34.0, first on a small sandbox (2 vCPU, 4 GB), then resized to default (4 vCPU, 8 GB). Single runs unless noted; your numbers will differ.
| Step | Result |
|---|---|
| Install the Temporal CLI | 1.3 s |
Server start to SERVING (3 restarts) |
1.43, 1.53, 1.49 s |
| One workflow with one activity, inside the sandbox (50 runs) | median 98 ms, min 47 ms |
Same workflow from a laptop through sbx forward (20 runs) |
median 254 ms |
| Web UI on a private hosted URL | 401 without the token, 302 then 200 with it |
| Memory after start | server 185 MB RSS, Python worker 60 MB |
sbx stop --wait then sbx resume (memory restored) |
Server and worker kept running; history intact |
| Resume on a bigger size (boots from disk) | systemd restarted both; all 54 workflows still there |
Restart without --db-filename |
1 workflow before, 0 after |
| Timer that came due while stopped | Fired about 96 s after a memory-restored resume; within 1 to 2 s after a server restart or cold boot |
The last row is the one to read before you rely on timers. It is explained in Timers that come due while the sandbox is stopped.
1. Install the CLI and run the server under systemd#
Temporal's dev server is a single binary inside the Temporal CLI (install the CLI, server start-dev reference). Use its own install script:
sbx new --name temporal --type small --no-env
id=sb_0fcf3wsqh3 # the id that sbx new printed
sbx exec $id "curl -sSf https://temporal.download/cli.sh | sh && ~/.temporalio/bin/temporal --version"temporal: Temporal CLI installed at /home/user/.temporalio/bin/temporal
temporal version 1.9.1 (Server 1.32.0, UI 2.54.1)Run it as a systemd unit, not a detached shell command, so it comes back when the sandbox boots from disk. Save this as temporal.service:
[Unit]
Description=Temporal dev server
After=network-online.target
[Service]
User=user
ExecStart=/home/user/.temporalio/bin/temporal server start-dev --ip 127.0.0.1 --db-filename /home/user/temporal.db --ui-disable-news-fetch
Restart=always
[Install]
WantedBy=multi-user.targetsbx scp ./temporal.service $id:/home/user/
sbx exec $id "sudo mv temporal.service /etc/systemd/system/ && sudo systemctl daemon-reload && sudo systemctl enable --now temporal"
sbx exec $id "until ~/.temporalio/bin/temporal operator cluster health 2>/dev/null | grep -q SERVING; do sleep 0.2; done; ss -ltn | grep -E '7233|8233'"LISTEN 0 4096 127.0.0.1:7233 0.0.0.0:*
LISTEN 0 4096 127.0.0.1:8233 0.0.0.0:*Three choices in that unit:
--db-filename. Without it, the dev server keeps everything in memory. We checked: a second server without the flag had 1 workflow before a restart and 0 after. With the flag, history lives in a SQLite file on the sandbox disk (1.7 MB after 56 workflows). It didn't slow anything down: 50 workflows took a median 98 ms with the file and 100 ms without it.--ip 127.0.0.1. The gRPC port (7233) and the UI (8233) listen inside the VM only. The Web UI has no login in dev mode ("Auth":{"Enabled":false}in its settings), so you reach it through a private hosted URL (step 3), and the gRPC port through a tunnel (step 4).--ui-disable-news-fetch. The UI stops calling out for its news feed. Optional.
2. Run a workflow#
Save the workflow, the worker and a starter. ReminderWorkflow is a durable timer; you will use it in step 5.
# workflows.py
from datetime import timedelta
from temporalio import activity, workflow
@activity.defn
async def greet(name: str) -> str:
return f"Hello, {name}!"
@workflow.defn
class HelloWorkflow:
@workflow.run
async def run(self, name: str) -> str:
return await workflow.execute_activity(
greet, name, start_to_close_timeout=timedelta(seconds=10)
)
@workflow.defn
class ReminderWorkflow:
@workflow.run
async def run(self, minutes: int) -> str:
started = workflow.now()
await workflow.sleep(timedelta(minutes=minutes))
waited = (workflow.now() - started).total_seconds()
return f"woke after {waited:.0f} s of workflow time"# worker.py
import asyncio
from temporalio.client import Client
from temporalio.worker import Worker
from workflows import HelloWorkflow, ReminderWorkflow, greet
async def main():
client = await Client.connect("127.0.0.1:7233")
worker = Worker(client, task_queue="demo",
workflows=[HelloWorkflow, ReminderWorkflow], activities=[greet])
print("worker started on task queue 'demo'", flush=True)
await worker.run()
asyncio.run(main())# starter.py
import asyncio, os, statistics, sys, time, uuid
from temporalio.client import Client
from workflows import HelloWorkflow
async def main():
client = await Client.connect(os.environ.get("TEMPORAL_ADDRESS", "127.0.0.1:7233"))
n = int(sys.argv[1]) if len(sys.argv) > 1 else 1
times = []
for i in range(n):
t0 = time.perf_counter()
result = await client.execute_workflow(
HelloWorkflow.run, "sandbox", id=f"hello-{uuid.uuid4().hex[:8]}", task_queue="demo")
times.append((time.perf_counter() - t0) * 1000)
print(result)
print(f"{n} workflows, median {statistics.median(times):.1f} ms, "
f"min {min(times):.1f} ms, max {max(times):.1f} ms")
asyncio.run(main())Install the SDK (Temporal Python SDK) in a virtual environment and run the worker as a second unit. Requires=temporal.service restarts the worker whenever you restart the server:
# temporal-worker.service
[Unit]
Description=Temporal worker (task queue demo)
After=temporal.service
Requires=temporal.service
[Service]
User=user
WorkingDirectory=/home/user/app
ExecStart=/home/user/app/.venv/bin/python worker.py
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.targetsbx exec $id "mkdir -p app"
for f in workflows.py worker.py starter.py temporal-worker.service; do sbx scp ./$f $id:/home/user/app/; done
sbx exec $id --timeout 600 "cd app && python3 -m venv .venv && .venv/bin/pip install -q temporalio"
sbx exec $id "sudo mv app/temporal-worker.service /etc/systemd/system/ && sudo systemctl daemon-reload && sudo systemctl enable --now temporal-worker"
sbx exec $id "cd app && .venv/bin/python starter.py 50"Hello, sandbox!
50 workflows, median 98.0 ms, min 47.0 ms, max 169.9 msThat time covers start, a workflow task, the activity, a second workflow task and the result. The pip install took 2.5 s; sbx exec stops a command after 30 s by default, so keep --timeout 600 on installs.
The CLI talks to the same server:
sbx exec $id "export PATH=\$PATH:~/.temporalio/bin
temporal workflow start --type ReminderWorkflow --task-queue demo --workflow-id reminder-1 --input 3
temporal workflow list --limit 3" Status WorkflowId Type StartTime
Running reminder-1 ReminderWorkflow now
Completed hello-6a249bde HelloWorkflow 8 seconds ago
Completed hello-846bcce8 HelloWorkflow 8 seconds ago3. Open the Web UI on a private URL#
The UI listens on 127.0.0.1:8233, so host it with --localhost (hosting docs):
sbx host $id 8233 --localhost --title "Temporal UI"{"ok":true,"type":"port.hosted","sandboxId":"sb_0fcf3wsqh3","port":8233,"url":"https://direct-humble-cabin-8233.box.sandboxer.com?_token=<token>","access":"private","isProtected":true,"localhost":true}Open that URL in a browser. We checked it with curl, using a cookie jar for the session cookie that the token sets:
B=https://direct-humble-cabin-8233.box.sandboxer.com
curl -s -o /dev/null -w "no token: %{http_code}\n" $B/
curl -s -o /dev/null -w "with token: %{http_code}\n" -c jar.txt "$B/?_token=<token>"
curl -s -b jar.txt "$B/api/v1/namespaces/default/workflows?query=WorkflowType%3D%22ReminderWorkflow%22"no token: 401
with token: 302
{
"executions": [
{
"execution": {
"workflowId": "reminder-1",
…
"status": "WORKFLOW_EXECUTION_STATUS_RUNNING",The UI's own /api/v1 passes through the same URL, which is how the browser UI lists, describes and can cancel or terminate workflows. Anyone with the full URL can do all of that, so keep it private. --public would hand that control to anyone who finds the hostname.
4. Connect from your laptop#
A worker or client on your laptop needs the gRPC port, not the UI. Forward it with sbx forward and point the client at the local port:
sbx forward $id --remote 7233 --local 17233 &
TEMPORAL_ADDRESS=127.0.0.1:17233 python starter.py 20Hello, sandbox!
20 workflows, median 254.1 ms, min 253.1 ms, max 262.2 msThe extra 155 ms is the network path from our laptop to the sandbox, which depends on where you are. For the worker, keep it next to the server in the sandbox, as above.
5. What survives stop and resume#
We ran three cases, each with a ReminderWorkflow sleeping when the sandbox stopped.
Memory restored. sbx stop $id --wait (2.2 s) and sbx resume $id (1.5 s, memoryRestored: true). systemd's ActiveEnterTimestamp for the server was unchanged: the processes were never restarted, the history was all there, and the hosted URL and its cookie worked again. While the sandbox was stopped, the URL answered 404.
Boot from disk. sbx resume $id --type default (memoryRestored: false, resumeReason: "resized"). Both units started at boot, the server read temporal.db, and temporal workflow count still said Total: 54. This is the case --db-filename and systemd are for. It also happens when no machine with the same CPU type is free. Moving to a smaller size isn't supported yet; to shrink, create a new sandbox and copy temporal.db.
Without --db-filename. A restart alone emptied the server. A boot from disk would do the same.
Timers that come due while the sandbox is stopped#
No timer was lost in any run. But in our runs on October 27, 2026, a timer that came due while the sandbox was stopped didn't always fire at once. This is what we observed, not documented behavior:
| Timer | Stopped for | How the sandbox came back | Fired |
|---|---|---|---|
| 3 min | 2 min 49 s | Memory restored | 96 s after resume |
| 3 min | 2 min 58 s | Memory restored | 96 s after resume |
| 1 min | 1 min 33 s | Memory restored | within 2 s of resume |
| 1 min | 1 min 14 s | Boot from disk | within 1 s of the server starting |
| 3 min | 2 min 49 s | Memory restored, then sudo systemctl restart temporal |
2 s after the restart |
The general rule: after a resume with memory, timers based on elapsed time don't count the stopped time; restart time-sensitive services or schedule by wall clock. In our session, uptime -s moved 108 s later across a 169 s stop, so elapsed time inside the VM counted only about 61 s of it. The server's in-process wake-up for the next timer runs on elapsed time, so it came late. On a fresh start, the server reads its due timers from the database and fires the overdue ones at once.
The workflow result shows the effect: workflow.now() is wall-clock time, so a 3-minute reminder reported woke after 296 s of workflow time.
If timers must fire promptly after a resume, restart the server as part of your resume step. It takes about 1.5 s, and the worker restarts with it:
sbx resume $id
sbx exec $id "sudo systemctl restart temporal"6. Clean up#
sbx delete $id --yesA small sandbox bills at half the default rate of $0.036/hour while it runs, and nothing while it's stopped.
FAQ#
Is the dev server fit for production?#
No. Temporal documents start-dev as a local development server with a SQLite store. Use it for development, tests, demos and agent experiments. For production, use Temporal Cloud or a self-hosted cluster as described in Temporal's docs.
Which size do I need?#
small (2 vCPU, 4 GB) was enough here. After about 120 workflows, the server used 185 MB of memory and the Python worker 60 MB. Size up for your own activities, not for the server.
Why does the UI return 401?#
The hosted URL is private by default. Open the full URL with ?_token= once; it sets a 30-day cookie and the UI works after that. A request without the token never reaches Temporal.
Do I lose my workflow history when the sandbox auto-stops?#
Not with --db-filename. Auto-stop is a normal stop: a memory-restored resume keeps the running server, and a boot from disk reloads the SQLite file. Without the flag, history survives only a memory-restored resume.
Can my laptop's worker poll the sandbox server?#
Yes, through sbx forward to port 7233, as in step 4. The hosted URL is HTTPS for the UI; we didn't test gRPC through it.
Related#
More guides
For running AI-written code we rank Sandboxer, Freestyle and E2B first: each gives code its own VM with an API or SDK and can pause with memory. Pick Daytona for Windows or GPUs, Modal or Northflank for GPUs at scale, E2B or Northflank to run in your own cloud, and Vercel Sandbox if your app lives on Vercel.
For coding agents we rank Sandboxer first: a full Ubuntu VM with Claude Code and Codex preinstalled, Docker, and stop-and-resume that keeps the agent's session in memory. Freestyle and Amp Orbs also keep memory across pauses. Pick E2B for prebuilt agent templates on open source, Docker Sandboxes for free local runs, and Codespaces if you live in GitHub.
The best Daytona alternatives depend on why you're switching. For a full Linux microVM on every sandbox with memory-saving stop and Docker, we rank Sandboxer first. E2B suits open source and self-hosting, Modal massive short-job fleets, Northflank bring-your-own-cloud with GPUs, and Vercel Sandbox apps on Vercel.
If you're leaving E2B for longer sessions or a fuller machine, we rank Sandboxer first, then Freestyle: both give each sandbox a full Linux VM that keeps memory across pauses, with Docker and no short session cap. Pick Daytona for Windows or GPUs, Northflank for bring-your-own-cloud, and stay on E2B if you need open source.