Every Python run executes in a single-use gVisor sandbox that is created for that run and destroyed after it. The sandbox has its own network boundary, enforced outside the Python process, so a run cannot widen it. This page describes what that boundary allows, what it refuses, and how each refusal looks from your code.

What works

  • Outbound TCP to public IPv4 addresses, on any port. HTTPS on 443, Postgres on 5432, and anything else that speaks TCP — including plain HTTP on port 80, which stays open because not every system a pipeline has to talk to offers TLS.
  • DNS, resolved through the cluster resolver. Use hostnames; you do not need to hardcode addresses.
  • Preinstalled and declared dependencies. The packages in the sandbox image are available immediately, and a node’s requirements are resolved to binary wheels before the run and installed offline inside the sandbox.
Plain HTTP is not encrypted. Anything a run sends over http:// — or to any other port without TLS — is readable, and modifiable, by every network between this cluster and the destination. Treat credentials, tokens and personal data sent that way as disclosed. Prefer https:// whenever the far end supports it; the usual way this goes wrong is a connection string with a password in it, sent to a legacy endpoint over http://.

Call a public API

Read from a public database

Use a secret reference for the connection string. Never inline credentials in node code. If the secret is a connection (a structured secret with a host, port, database, user and password rather than a bare connection string), the runtime assembles the connection string for you — TLS required unless the connection says otherwise — and exposes it two ways: as os.environ["<ENV>_URL"], and as get_secret("<ENV>")["url"] alongside the individual fields.

Declare a dependency

Requirements are resolved against PyPI before the sandbox starts and installed from prebuilt wheels with no index access and no build step. A package with no binary wheel for the sandbox’s Python version and platform fails at resolution, not at import — the run never starts. Ask your operator to add such a package to the image instead.

What does not work

To read or write Panels data, do not call the platform API from inside a run. Use the run’s inputs (input_table()), the DuckLake run proxy that the sandbox is already wired to, and output() / output_table() for results. Those go through run-scoped credentials that expire with the run and are refused from outside the cluster.
Requests leave from a datacenter IP address. Sites with bot protection may challenge or block them even though the network boundary allows the connection. A 403 with a challenge page is a decision by the remote site, not a sandbox restriction.

Profile differences

The profile for a run is derived from what the node declares; you do not select it directly. On governed, an attempt to create any IP socket raises PermissionError: [Errno 1] Operation not permitted — the kernel refuses it, and the refusal is inherited by every child process. See Connect an OAuth provider for the request API available on that tier.

Timeouts

Your code gets five minutes of execution, and that is the budget every network call it makes has to fit inside. Dependency installation and interpreter startup are billed to separate budgets before yours begins — 2 minutes and 60 seconds — so they do not spend it. On a managed runner the five minutes is a ceiling rather than a promise. The runner’s claim is capped at five minutes and covers installation, startup and execution together, so your code gets what is left of the claim once preparation and startup have taken their share. A run that installs nothing starts with almost the whole five minutes; one that resolves a large dependency set starts with less. Plan for the remainder, not the ceiling. A long sync is not meant to fit one run at all. End the run early with ctx.request_continuation(...) and the platform runs the node again from the cursor you checkpointed — see Connect a database. Set an explicit client timeout well inside your budget. Most HTTP clients default to no timeout at all, which turns an unreachable host into a run that ends at its deadline with no result:

Why IPv6 is not enabled

IPv6 egress is deliberately off, and the sandbox refuses AF_INET6 sockets early so the failure is legible rather than a hang. The egress boundary is an address-based allowance: public destinations are permitted and private, loopback, link-local, metadata, carrier-grade NAT, test, multicast, and reserved ranges are denied. That classification is only sound if the address being classified is the address actually reached. With IPv6 in play, a translated destination (NAT64 and similar) can carry a private target inside a public-looking address, and the prefix used for the translation is deployment-specific — a fixed exception list cannot cover it. Enabling IPv6 therefore requires forcing traffic through a gateway that classifies the post-translation address. Until that exists, IPv6 stays disabled rather than being allowed with a boundary that cannot be trusted. Practically, every destination must have an A record. Libraries that try IPv6 first and fall back to IPv4 (httpx, requests, urllib3, socket.create_connection) continue to work: the IPv6 attempt fails at socket creation and the IPv4 attempt proceeds.

Troubleshooting