Versions
@cloudflare/sandbox 0.12.5, wrangler 4.123.0, wrangler dev --local (WSL2 + Docker Desktop)
Describe the bug
sandbox.tunnels.get(port) is not idempotent under wrangler dev --local: after the first successful call establishes a healthy quick tunnel, a second tunnels.get(port) for the same port immediately throws:
OperationInterruptedError: Tunnel recovery attempts were exhausted
The tunnel itself is still healthy at that moment (the *.trycloudflare.com URL keeps serving 200s), yet every subsequent tunnels.get on that Durable Object keeps failing with the same error. The exhausted state never resets on its own — only destroying and recreating the sandbox (stop → start) clears it.
The same happens after any transient failure (e.g. the target port briefly not listening): recovery attempts get consumed and the counter never resets even after the origin becomes healthy, so the DO is permanently unable to return a tunnel URL.
Expected behavior
tunnels.get(port) should be idempotent per port (return the existing healthy tunnel), and the recovery-attempt counter should reset after a successful connection — or at least be resettable without recycling the sandbox.
Repro
const sandbox = getSandbox(env.Sandbox, 'repro')
await sandbox.startProcess('python3 -m http.server 8080', { processId: 'srv' })
const a = await sandbox.tunnels.get(8080) // ok, URL works
const b = await sandbox.tunnels.get(8080) // throws OperationInterruptedError
Workaround
Cache the tunnel URL out-of-band (we write it to a file in the container) and only call tunnels.get when the dev-server process is (re)started; never call it twice per container lifetime.
Versions
@cloudflare/sandbox 0.12.5, wrangler 4.123.0,
wrangler dev --local(WSL2 + Docker Desktop)Describe the bug
sandbox.tunnels.get(port)is not idempotent underwrangler dev --local: after the first successful call establishes a healthy quick tunnel, a secondtunnels.get(port)for the same port immediately throws:The tunnel itself is still healthy at that moment (the
*.trycloudflare.comURL keeps serving 200s), yet every subsequenttunnels.geton that Durable Object keeps failing with the same error. The exhausted state never resets on its own — only destroying and recreating the sandbox (stop → start) clears it.The same happens after any transient failure (e.g. the target port briefly not listening): recovery attempts get consumed and the counter never resets even after the origin becomes healthy, so the DO is permanently unable to return a tunnel URL.
Expected behavior
tunnels.get(port)should be idempotent per port (return the existing healthy tunnel), and the recovery-attempt counter should reset after a successful connection — or at least be resettable without recycling the sandbox.Repro
Workaround
Cache the tunnel URL out-of-band (we write it to a file in the container) and only call
tunnels.getwhen the dev-server process is (re)started; never call it twice per container lifetime.