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
requirementsare resolved to binary wheels before the run and installed offline inside the sandbox.
Call a public API
Read from a public database
os.environ["<ENV>_URL"], and as
get_secret("<ENV>")["url"] alongside the individual fields.
Declare a dependency
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 withctx.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 refusesAF_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.