diff --git a/docs/sandbox.md b/docs/sandbox.md
index 186c186a..0efaa75c 100644
--- a/docs/sandbox.md
+++ b/docs/sandbox.md
@@ -1004,7 +1004,8 @@ def create(cls,
delete_after_delay: int = 0,
delete_after_inactivity_delay: int = 0,
app_id: Optional[str] = None,
- enable_mesh: bool = None) -> Sandbox
+ enable_mesh: bool = None,
+ poll_interval: float = DEFAULT_POLL_INTERVAL) -> Sandbox
```
Create a new sandbox instance.
@@ -1039,6 +1040,7 @@ Create a new sandbox instance.
after this many seconds.
- `app_id` - If provided, create the sandbox service in an existing app instead of creating a new one.
- `enable_mesh` - Enable or disable mesh for this sandbox. Disabled by default
+- `poll_interval` - Time between health checks in seconds when wait_ready is True (default: 0.5)
**Returns**:
@@ -1096,15 +1098,17 @@ Get a sandbox by service ID.
```python
def wait_ready(timeout: int = DEFAULT_INSTANCE_WAIT_TIMEOUT,
- poll_interval: float = DEFAULT_POLL_INTERVAL) -> bool
+ poll_interval: Optional[float] = None) -> bool
```
-Wait for sandbox to become ready with proper polling.
+Wait for sandbox to become ready with exponential backoff polling.
+
+Starts polling at 0.1s intervals, doubling each time up to poll_interval.
**Arguments**:
- `timeout` - Maximum time to wait in seconds
-- `poll_interval` - Time between health checks in seconds
+- `poll_interval` - Maximum time between health checks in seconds (defaults to instance poll_interval)
**Returns**:
@@ -1117,19 +1121,18 @@ Wait for sandbox to become ready with proper polling.
```python
def wait_tcp_proxy_ready(timeout: int = DEFAULT_INSTANCE_WAIT_TIMEOUT,
- poll_interval: float = DEFAULT_POLL_INTERVAL) -> bool
+ poll_interval: Optional[float] = None) -> bool
```
Wait for TCP proxy to become ready and available.
-Polls the deployment metadata until the TCP proxy information is available.
-This is useful when enable_tcp_proxy=True was set during sandbox creation,
-as the proxy information may not be immediately available.
+Polls the deployment metadata with exponential backoff until the TCP proxy
+information is available. Starts at 0.1s intervals, doubling up to poll_interval.
**Arguments**:
- `timeout` - Maximum time to wait in seconds
-- `poll_interval` - Time between checks in seconds
+- `poll_interval` - Maximum time between checks in seconds (defaults to instance poll_interval)
**Returns**:
@@ -1166,12 +1169,13 @@ def get_domain() -> Optional[str]
Get the public domain of the sandbox.
-Returns the domain name (e.g., "app-name-org.koyeb.app") without protocol or path.
-To construct the URL, use: f"https://{sandbox.get_domain()}"
+Returns the domain (e.g., "app-name-org.koyeb.app/r/routing_key/" or
+"app-name-org.koyeb.app") without protocol. To get the full URL with protocol,
+use sandbox._get_url()
**Returns**:
-- `Optional[str]` - The domain name or None if unavailable
+- `Optional[str]` - The domain or None if unavailable
@@ -1536,7 +1540,8 @@ async def create(cls,
delete_after_delay: int = 0,
delete_after_inactivity_delay: int = 0,
app_id: Optional[str] = None,
- enable_mesh: bool = False) -> AsyncSandbox
+ enable_mesh: bool = False,
+ poll_interval: float = DEFAULT_POLL_INTERVAL) -> AsyncSandbox
```
Create a new sandbox instance with async support.
@@ -1573,6 +1578,7 @@ Create a new sandbox instance with async support.
after this many seconds.
- `app_id` - If provided, create the sandbox service in an existing app instead of creating a new one.
- `enable_mesh` - Enable or disable mesh for this sandbox. Disabled by default
+- `poll_interval` - Time between health checks in seconds when wait_ready is True (default: 0.5)
**Returns**:
@@ -1591,15 +1597,17 @@ Create a new sandbox instance with async support.
```python
async def wait_ready(timeout: int = DEFAULT_INSTANCE_WAIT_TIMEOUT,
- poll_interval: float = DEFAULT_POLL_INTERVAL) -> bool
+ poll_interval: Optional[float] = None) -> bool
```
-Wait for sandbox to become ready with proper async polling.
+Wait for sandbox to become ready with exponential backoff async polling.
+
+Starts polling at 0.1s intervals, doubling each time up to poll_interval.
**Arguments**:
- `timeout` - Maximum time to wait in seconds
-- `poll_interval` - Time between health checks in seconds
+- `poll_interval` - Maximum time between health checks in seconds (defaults to instance poll_interval)
**Returns**:
@@ -1611,21 +1619,19 @@ Wait for sandbox to become ready with proper async polling.
#### wait\_tcp\_proxy\_ready
```python
-async def wait_tcp_proxy_ready(
- timeout: int = DEFAULT_INSTANCE_WAIT_TIMEOUT,
- poll_interval: float = DEFAULT_POLL_INTERVAL) -> bool
+async def wait_tcp_proxy_ready(timeout: int = DEFAULT_INSTANCE_WAIT_TIMEOUT,
+ poll_interval: Optional[float] = None) -> bool
```
Wait for TCP proxy to become ready and available asynchronously.
-Polls the deployment metadata until the TCP proxy information is available.
-This is useful when enable_tcp_proxy=True was set during sandbox creation,
-as the proxy information may not be immediately available.
+Polls the deployment metadata with exponential backoff until the TCP proxy
+information is available. Starts at 0.1s intervals, doubling up to poll_interval.
**Arguments**:
- `timeout` - Maximum time to wait in seconds
-- `poll_interval` - Time between checks in seconds
+- `poll_interval` - Maximum time between checks in seconds (defaults to instance poll_interval)
**Returns**:
@@ -2235,6 +2241,9 @@ def health() -> Dict[str, str]
Check the health status of the server.
+Uses a short timeout and no retries since callers (wait_ready)
+already handle polling with backoff.
+
**Returns**:
Dict with status information
@@ -2243,6 +2252,7 @@ Check the health status of the server.
**Raises**:
- `requests.HTTPError` - If the health check fails
+- `requests.Timeout` - If the health check times out
diff --git a/examples/02_create_sandbox_with_timing.py b/examples/02_create_sandbox_with_timing.py
index 492779c5..a39fc376 100644
--- a/examples/02_create_sandbox_with_timing.py
+++ b/examples/02_create_sandbox_with_timing.py
@@ -88,6 +88,7 @@ def main(run_long_tests=False):
name=f"example-sandbox-timed-{suffix}",
wait_ready=True,
api_token=api_token,
+ # poll_interval=0.1,
)
create_duration = time.time() - create_start
tracker.record("Sandbox creation", create_duration, "setup")
diff --git a/koyeb/sandbox/executor_client.py b/koyeb/sandbox/executor_client.py
index 73a687f8..5525e39c 100644
--- a/koyeb/sandbox/executor_client.py
+++ b/koyeb/sandbox/executor_client.py
@@ -158,15 +158,20 @@ def health(self) -> Dict[str, str]:
"""
Check the health status of the server.
+ Uses a short timeout and no retries since callers (wait_ready)
+ already handle polling with backoff.
+
Returns:
Dict with status information
Raises:
requests.HTTPError: If the health check fails
+ requests.Timeout: If the health check times out
"""
- response = self._request_with_retry(
- "GET", f"{self.base_url}/health", timeout=self.timeout
+ response = self._session.get(
+ f"{self.base_url}/health", timeout=5
)
+ response.raise_for_status()
return response.json()
def run(
diff --git a/koyeb/sandbox/sandbox.py b/koyeb/sandbox/sandbox.py
index 16b2ab7e..7ce8773e 100644
--- a/koyeb/sandbox/sandbox.py
+++ b/koyeb/sandbox/sandbox.py
@@ -83,6 +83,7 @@ def __init__(
name: Optional[str] = None,
api_token: Optional[str] = None,
sandbox_secret: Optional[str] = None,
+ poll_interval: float = DEFAULT_POLL_INTERVAL,
):
self.sandbox_id = sandbox_id
self.app_id = app_id
@@ -90,6 +91,7 @@ def __init__(
self.name = name
self.api_token = api_token
self.sandbox_secret = sandbox_secret
+ self.poll_interval = poll_interval
self._created_at = time.time()
self._sandbox_url: Optional[Tuple[str, Optional[str]]] = None
self._domain: Optional[str] = None
@@ -123,6 +125,7 @@ def create(
delete_after_inactivity_delay: int = 0,
app_id: Optional[str] = None,
enable_mesh: bool = None,
+ poll_interval: float = DEFAULT_POLL_INTERVAL,
) -> Sandbox:
"""
Create a new sandbox instance.
@@ -156,6 +159,7 @@ def create(
after this many seconds.
app_id: If provided, create the sandbox service in an existing app instead of creating a new one.
enable_mesh: Enable or disable mesh for this sandbox. Disabled by default
+ poll_interval: Time between health checks in seconds when wait_ready is True (default: 0.5)
Returns:
Sandbox: A new Sandbox instance
@@ -200,6 +204,7 @@ def create(
delete_after_inactivity_delay=delete_after_inactivity_delay,
app_id=app_id,
enable_mesh=enable_mesh,
+ poll_interval=poll_interval,
)
if wait_ready:
@@ -234,6 +239,7 @@ def _create_sync(
delete_after_inactivity_delay: int = 0,
app_id: Optional[str] = None,
enable_mesh: bool = None,
+ poll_interval: float = DEFAULT_POLL_INTERVAL,
) -> Sandbox:
"""
Synchronous creation method that returns creation parameters.
@@ -302,6 +308,7 @@ def _create_sync(
name=name,
api_token=api_token,
sandbox_secret=sandbox_secret,
+ poll_interval=poll_interval,
)
@classmethod
@@ -383,20 +390,25 @@ def get_from_id(
def wait_ready(
self,
timeout: int = DEFAULT_INSTANCE_WAIT_TIMEOUT,
- poll_interval: float = DEFAULT_POLL_INTERVAL,
+ poll_interval: Optional[float] = None,
) -> bool:
"""
- Wait for sandbox to become ready with proper polling.
+ Wait for sandbox to become ready with exponential backoff polling.
+
+ Starts polling at 0.1s intervals, doubling each time up to poll_interval.
Args:
timeout: Maximum time to wait in seconds
- poll_interval: Time between health checks in seconds
+ poll_interval: Maximum time between health checks in seconds (defaults to instance poll_interval)
Returns:
bool: True if sandbox became ready, False if timeout
"""
+ if poll_interval is None:
+ poll_interval = self.poll_interval
start_time = time.time()
sandbox_url = None
+ current_interval = 0.1
while time.time() - start_time < timeout:
# Get sandbox URL on first iteration or if not yet retrieved
@@ -404,7 +416,8 @@ def wait_ready(
sandbox_url = self._get_sandbox_url()
# If URL is not available yet, wait and retry
if sandbox_url is None:
- time.sleep(poll_interval)
+ time.sleep(current_interval)
+ current_interval = min(current_interval * 2, poll_interval)
continue
is_healthy = self.is_healthy()
@@ -412,37 +425,41 @@ def wait_ready(
if is_healthy:
return True
- time.sleep(poll_interval)
+ time.sleep(current_interval)
+ current_interval = min(current_interval * 2, poll_interval)
return False
def wait_tcp_proxy_ready(
self,
timeout: int = DEFAULT_INSTANCE_WAIT_TIMEOUT,
- poll_interval: float = DEFAULT_POLL_INTERVAL,
+ poll_interval: Optional[float] = None,
) -> bool:
"""
Wait for TCP proxy to become ready and available.
- Polls the deployment metadata until the TCP proxy information is available.
- This is useful when enable_tcp_proxy=True was set during sandbox creation,
- as the proxy information may not be immediately available.
+ Polls the deployment metadata with exponential backoff until the TCP proxy
+ information is available. Starts at 0.1s intervals, doubling up to poll_interval.
Args:
timeout: Maximum time to wait in seconds
- poll_interval: Time between checks in seconds
+ poll_interval: Maximum time between checks in seconds (defaults to instance poll_interval)
Returns:
bool: True if TCP proxy became ready, False if timeout
"""
+ if poll_interval is None:
+ poll_interval = self.poll_interval
start_time = time.time()
+ current_interval = 0.1
while time.time() - start_time < timeout:
tcp_proxy_info = self.get_tcp_proxy_info()
if tcp_proxy_info is not None:
return True
- time.sleep(poll_interval)
+ time.sleep(current_interval)
+ current_interval = min(current_interval * 2, poll_interval)
return False
@@ -1050,6 +1067,7 @@ async def create(
delete_after_inactivity_delay: int = 0,
app_id: Optional[str] = None,
enable_mesh: bool = False,
+ poll_interval: float = DEFAULT_POLL_INTERVAL,
) -> AsyncSandbox:
"""
Create a new sandbox instance with async support.
@@ -1085,6 +1103,7 @@ async def create(
after this many seconds.
app_id: If provided, create the sandbox service in an existing app instead of creating a new one.
enable_mesh: Enable or disable mesh for this sandbox. Disabled by default
+ poll_interval: Time between health checks in seconds when wait_ready is True (default: 0.5)
Returns:
AsyncSandbox: A new AsyncSandbox instance
@@ -1122,6 +1141,7 @@ async def create(
delete_after_inactivity_delay=delete_after_inactivity_delay,
app_id=app_id,
enable_mesh=enable_mesh,
+ poll_interval=poll_interval,
),
)
@@ -1133,6 +1153,7 @@ async def create(
name=sync_result.name,
api_token=sync_result.api_token,
sandbox_secret=sync_result.sandbox_secret,
+ poll_interval=poll_interval,
)
sandbox._created_at = sync_result._created_at
@@ -1150,19 +1171,24 @@ async def create(
async def wait_ready(
self,
timeout: int = DEFAULT_INSTANCE_WAIT_TIMEOUT,
- poll_interval: float = DEFAULT_POLL_INTERVAL,
+ poll_interval: Optional[float] = None,
) -> bool:
"""
- Wait for sandbox to become ready with proper async polling.
+ Wait for sandbox to become ready with exponential backoff async polling.
+
+ Starts polling at 0.1s intervals, doubling each time up to poll_interval.
Args:
timeout: Maximum time to wait in seconds
- poll_interval: Time between health checks in seconds
+ poll_interval: Maximum time between health checks in seconds (defaults to instance poll_interval)
Returns:
bool: True if sandbox became ready, False if timeout
"""
+ if poll_interval is None:
+ poll_interval = self.poll_interval
start_time = time.time()
+ current_interval = 0.1
while time.time() - start_time < timeout:
loop = asyncio.get_running_loop()
@@ -1171,30 +1197,33 @@ async def wait_ready(
if is_healthy:
return True
- await asyncio.sleep(poll_interval)
+ await asyncio.sleep(current_interval)
+ current_interval = min(current_interval * 2, poll_interval)
return False
async def wait_tcp_proxy_ready(
self,
timeout: int = DEFAULT_INSTANCE_WAIT_TIMEOUT,
- poll_interval: float = DEFAULT_POLL_INTERVAL,
+ poll_interval: Optional[float] = None,
) -> bool:
"""
Wait for TCP proxy to become ready and available asynchronously.
- Polls the deployment metadata until the TCP proxy information is available.
- This is useful when enable_tcp_proxy=True was set during sandbox creation,
- as the proxy information may not be immediately available.
+ Polls the deployment metadata with exponential backoff until the TCP proxy
+ information is available. Starts at 0.1s intervals, doubling up to poll_interval.
Args:
timeout: Maximum time to wait in seconds
- poll_interval: Time between checks in seconds
+ poll_interval: Maximum time between checks in seconds (defaults to instance poll_interval)
Returns:
bool: True if TCP proxy became ready, False if timeout
"""
+ if poll_interval is None:
+ poll_interval = self.poll_interval
start_time = time.time()
+ current_interval = 0.1
while time.time() - start_time < timeout:
loop = asyncio.get_running_loop()
@@ -1204,7 +1233,8 @@ async def wait_tcp_proxy_ready(
if tcp_proxy_info is not None:
return True
- await asyncio.sleep(poll_interval)
+ await asyncio.sleep(current_interval)
+ current_interval = min(current_interval * 2, poll_interval)
return False
diff --git a/koyeb/sandbox/utils.py b/koyeb/sandbox/utils.py
index ba556539..dd1d46b5 100644
--- a/koyeb/sandbox/utils.py
+++ b/koyeb/sandbox/utils.py
@@ -41,7 +41,7 @@
MIN_PORT = 1
MAX_PORT = 65535
DEFAULT_INSTANCE_WAIT_TIMEOUT = 60 # seconds
-DEFAULT_POLL_INTERVAL = 2.0 # seconds
+DEFAULT_POLL_INTERVAL = 0.5 # seconds
DEFAULT_COMMAND_TIMEOUT = 30 # seconds
DEFAULT_HTTP_TIMEOUT = 30 # seconds for HTTP requests