Skip to content

Retries and idempotency

The Poli Page Python SDK retries transient failures automatically. Every POST carries an Idempotency-Key that stays the same across the retries of one call. The API does not deduplicate on it, so a retried render can store a second document (see Idempotency).

  • network_error, timeout, 5xx responses (such as 503 PUBLISH_IN_PROGRESS) and 429 responses (such as RATE_LIMIT_EXCEEDED) are retried.
  • QUOTA_EXCEEDED and OVERAGE_CAP_EXCEEDED (429) and STORAGE_ACCESS_DENIED (502) are not retried: they last until the monthly reset or until the bucket access is restored, so the SDK raises them at once and is_retryable() is False.
  • Up to max_retries retries (default 2 — three total attempts); after the last one, the last error is raised.
  • Exponential backoff starting at retry_delay seconds (default 0.5), doubling each attempt, with a random factor in [0.5, 1.5).
  • Honors the Retry-After response header when present (integer seconds or an HTTP-date), clamped to 0–30 seconds, without jitter. Retry-After-Ms is ignored.
  • Caller cancellation (asyncio.CancelledError, KeyboardInterrupt) is never retried. The presigned-URL download is never retried.
  • DOWNLOAD_FAILED is never retried and is_retryable() is False for it, whatever the storage status, a download timeout included.
  • invalid_response (a 2xx whose body is not JSON) and io_error (render_to_file could not write the file) are never retried and is_retryable() is False for them.

Set max_retries=0 on the constructor to disable retries entirely.

from poli_page import PoliPage
client = PoliPage(max_retries=0)

Every POST request (render.*, documents.thumbnails) includes an Idempotency-Key header: a lowercase UUID v4 the SDK generates once per call and sends unchanged on every retry of that call. GET and DELETE requests and the presigned-URL download send none.

The API does not deduplicate on this header. Every POST /v1/render that reaches the renderer stores a new document, so a retry can create a second one when the first attempt was stored but its answer was lost: a 5xx raised after the document was stored, a timeout that fired while the API was still rendering (for example timeout=10 on a 15 s render), or a network_error after the request reached the API. A 503 PUBLISH_IN_PROGRESS is raised before any render work, so retrying it never duplicates. If an extra stored document matters to you, lower max_retries or raise timeout; note that max_retries=0 also stops the always-safe retry of PUBLISH_IN_PROGRESS.

You can supply your own key via idempotency_key on the input, for example to correlate a call in your logs; it is sent as is on every attempt of that call:

pdf = client.render.pdf({
"project": "billing",
"template": "invoice",
"data": {"invoiceNumber": "INV-001", "total": 1280},
"idempotency_key": "invoice-INV-001-v1",
})

Two calls with the same key are still two renders: each stores its own document.

from poli_page import PoliPage
client = PoliPage(max_retries=4, retry_delay=0.25)
pdf = client.render.pdf({
"project": "billing",
"template": "invoice",
"data": {"invoiceNumber": "INV-001", "total": 1280},
})