Retry and Timeout Behaviour
The Saner Python SDK includes built-in retry and timeout controls to ensure reliable API communication in production environments. These mechanisms help handle temporary failures such as network interruptions, service instability, rate limiting, or transient server errors without requiring manual retry logic in your scripts.
This page explains how retries work, which operations are eligible for a retry, how exponential backoff and jitter are applied, and how to configure retry behavior. Everything described here applies identically whether you're using SanerClient, PlatformClient, or CvemClient. See the "Client Types" guide if you haven't yet chosen between them; retry behavior isn't part of that decision.
Automatic Retries
By default, the SDK automatically retries failed requests when it detects temporary issues that are safe to retry.
Retries are triggered for:
- connection interruptions
- request timeouts
- HTTP 429 (rate limited): always retried, honoring
Retry-Afterwhen the server provides it - HTTP 500 errors
- HTTP 502 errors
- HTTP 503 errors
- HTTP 504 errors
Retries are not performed for:
- authentication failures (401, 403)
- client errors (400, 404, 422)
- SSL certificate errors
- unexpected runtime exceptions
This ensures incorrect requests fail immediately instead of retrying unnecessarily.
Important: Not every request above is retried under the same rules. Whether a write operation (one that creates or changes data) gets retried depends on whether repeating it is safe. See Retry Eligibility below. HTTP 429 is the one exception: it is always retried regardless of the operation type, because a 429 means the server refused to process the request at all.
Retry Eligibility: What Gets Retried
Retrying a request that already reached the server can be dangerous for anything that isn't safe to repeat: retrying a timed-out createRemediationJob call, for example, could create the same job twice if the first attempt actually succeeded server-side before the response was lost.
To avoid this, the SDK classifies every request as either safe to repeat or not safe to repeat, and only retries the failures that are safe to repeat, unless you explicitly opt in.
How the SDK decides
REST calls are classified by HTTP method, per RFC 9110:
- Safe to repeat:
GET,HEAD,OPTIONS,TRACE,PUT,DELETE - Not safe to repeat by default:
POST,PATCH
RPC calls have no HTTP method to inspect (every RPC call is sent as POST), so the SDK looks at the method name instead:
- Safe to repeat: method names starting with
get,is,list,fetch,download,check, orverify(case-insensitive) - Not safe to repeat by default: everything else, such as
addX,createX,updateX,deleteX,enforce,scan, and similar mutating operations
If an RPC method name doesn't match any of the read-only prefixes, the SDK treats it as not safe to repeat. When in doubt, it errs toward not duplicating a write rather than toward maximum resilience.
The eligibility table
| Failure type | Idempotent call (GET/PUT/DELETE, or RPC get*/is*/list*/…) | Write call (POST, or RPC add*/create*/…) |
|---|---|---|
| Connection never reached the server (DNS failure, refused connection, connect-phase timeout) | Retried | Retried, since the request never reached the server |
| Timeout after the request was sent (may have reached the server) | Retried | Not retried, unless retry_on_write=True |
| HTTP 500 / 502 / 503 / 504 | Retried | Not retried, unless retry_on_write=True |
| HTTP 429 (rate limited) | Retried | Retried: always, regardless of retry_on_write |
| HTTP 401 / 403 (auth) | Never retried | Never retried |
| HTTP 400 / 404 / 422 (client error) | Never retried | Never retried |
| SSL certificate error | Never retried | Never retried |
Opting in with retry_on_write
retry_on_writeIf you know a particular deployment is safe to retry regardless (for example, the server de-duplicates by an idempotency key on its side), you can restore the pre-existing "retry everything retryable" behavior:
from saner import SanerClient
client = SanerClient(
api_key="KEY",
accountid="Default",
retry_on_write=True,
)Default: retry_on_write = False.
Invalid Configuration Is Rejected Immediately
timeout, max_retries, and retry_backoff are validated the moment the client is constructed, not on the first request:
| Argument | Rejected when |
|---|---|
timeout | zero or negative |
max_retries | negative (0 is valid: it means "no retries") |
retry_backoff | negative (0 is valid: it means "retry immediately, no backoff") |
SanerClient(api_key="KEY", accountid="Default", max_retries=-1)
# ValueError: max_retries must be an integer >= 0 (0 disables retrying), got -1This matters because of how the retry loop is implemented: both transports loop max_retries + 1 times. A negative max_retries used to make that loop run zero times, so the call silently returned None without a request ever being sent: no exception, no log entry, nothing to indicate why. Raising ValueError at construction turns that into an error you see immediately, at the one place in your code you can actually fix it.
Timeout Control
The timeout parameter controls how long the SDK waits for a server response before terminating the request.
Default:
timeout = 30 secondsExample:
from saner import SanerClient
client = SanerClient(
api_key="KEY",
accountid="Default",
timeout=60
)Increase timeout when:
- retrieving large datasets
- exporting reports
- running slower network connections
Reduce timeout when:
- running CI/CD validation steps
- enforcing fast-fail automation behavior
Retry Count Control
The max_retries parameter controls how many retry attempts are made after the initial request fails.
Default:
max_retries = 3Example:
from saner import SanerClient
client = SanerClient(
api_key="KEY",
accountid="Default",
max_retries=5
)This means:
- 1 initial request
- up to 5 retry attempts
- total possible attempts = 6
Increasing retries improves resilience in unstable environments. Note that max_retries is the ceiling: a write operation that isn't retry-eligible (see above) can still fail after a single attempt regardless of this setting.
Exponential Backoff
The SDK applies exponential backoff between retry attempts to avoid overwhelming the server during failures.
Delay formula:
delay = retry_backoff × (2^attempt)A small randomized delay is also added automatically to prevent retry collisions across parallel scripts, and the total delay is capped so a single retry never sleeps indefinitely.
Default:
retry_backoff = 1.0
max backoff per attempt = 60 seconds (fixed)Example configuration:
from saner import SanerClient
client = SanerClient(
api_key="KEY",
accountid="Default",
max_retries=5,
retry_backoff=2
)Note: This 60-second cap also applies to server-supplied
Retry-Aftervalues (see Rate Limiting below). If a server asks the SDK to wait longer than 60 seconds, the SDK still waits only 60 seconds before its next attempt.
Retry Timing with Jitter (Randomized Delay)
In addition to exponential backoff, the SDK applies jitter using:
random.uniform(0, 0.5)This adds a small random delay between 0 and 0.5 seconds on top of the calculated retry interval.
Actual delay formula:
delay = min(retry_backoff × (2^attempt) + random.uniform(0, 0.5), 60)Example timing pattern (approximate):
| Attempt | Base Delay | With Jitter |
|---|---|---|
| Attempt 1 | 1s | ~1.2s |
| Attempt 2 | 2s | ~2.3s |
| Attempt 3 | 4s | ~4.1s |
| Attempt 4 | 8s | ~8.4s |
Each retry waits slightly differently, improving system stability during failures.
Why Jitter Matters
Without jitter, multiple scripts retry at the exact same time.
Example scenario:
Imagine 50 automation scripts:
- all send requests simultaneously
- all receive HTTP 503 errors
- all retry at exactly 1s, 2s, and 4s
This creates repeated traffic spikes that can overload the server again.
Jitter solves this by spreading retry timing across small random intervals:
Script A → retry at 1.1s
Script B → retry at 1.3s
Script C → retry at 1.4s
Script D → retry at 1.2sInstead of synchronized retry bursts, requests are distributed smoothly over time, improving reliability for large-scale automation environments.
Rate Limiting (HTTP 429) and Retry-After
Retry-AfterHTTP 429 is treated differently from other retryable statuses: it is always retried up to max_retries, regardless of the operation type or retry_on_write, because a 429 means the server refused to process the request, so there is nothing to duplicate.
If the server includes a Retry-After header on the 429 response, the SDK waits exactly that long (capped at 60 seconds) instead of computing its own exponential backoff for that attempt. Both forms of Retry-After are supported:
- delta-seconds:
Retry-After: 30 - an HTTP date:
Retry-After: Wed, 21 Oct 2026 07:28:00 GMT
If retries are exhausted while still rate limited, the SDK raises SanerRateLimitError (see the "Error Handling Model" guide), which carries the parsed retry_after value so your own code can decide how long to wait before trying again.
Retry Visibility in Logs
The Saner Python SDK integrates retry behavior directly with its structured logging system, allowing developers to track how many attempts were required to complete a request successfully. Each logged request includes an attempt field that indicates the exact retry iteration used for execution, and failures against a retryable HTTP status also include a retry_after field showing what the SDK parsed from the response (or null if none was present).
Example log entry:
{
"timestamp": "2026-04-03 14:28:14",
"method": "POST",
"params": null,
"json_body": {...},
"url": "https://eu.saner.secpod.com/CHScanner/getCHScoreSummaryForGroup",
"status": 200,
"response": {...},
"success": true,
"attempt": 1,
"time_ms": 837.37,
"accountid": "Default"
}The attempt value represents the retry sequence number:
| Attempt Value | Meaning |
|---|---|
| 1 | Request succeeded on first attempt |
| 2 | Request succeeded after one retry |
| 3+ | Request required multiple retries before success |
See the "Logging" guide for the full failure-entry field reference, including retry_after.
This makes it easy to identify transient network instability, retry-triggering server responses, or timeout conditions during automation workflows. When combined with structured timestamps and execution duration (time_ms), retry-aware logging provides full visibility into request reliability across environments.
When to Increase Retry Settings
Recommended scenarios:
Increase max_retries when:
- running scheduled automation jobs
- operating across unstable networks
- executing long batch workflows
- interacting with heavily loaded environments
Increase retry_backoff when:
- avoiding rate limits
- preventing retry bursts
- running parallel integrations at scale
Consider retry_on_write=True only when:
- you have confirmed the specific write operations you call are safe to repeat (for example, the server de-duplicates by name or by an idempotency key)
- you understand the tradeoff: resilience against transient failures, at the cost of a small chance of a duplicated write if a request actually reached the server before the failure
Production Example Configuration
Example resilient configuration suitable for automation pipelines:
from saner import SanerClient
client = SanerClient(
api_key="KEY",
accountid="Default",
timeout=60,
max_retries=5,
retry_backoff=2
)This configuration improves request reliability while maintaining controlled retry behavior across transient failures, and keeps the default safe behavior of not retrying writes that may have already reached the server.
If you have verified that retrying writes is safe for your integration:
from saner import SanerClient
client = SanerClient(
api_key="KEY",
accountid="Default",
timeout=60,
max_retries=5,
retry_backoff=2,
retry_on_write=True,
)Updated 5 days ago
