PDF rendering queue
GET /documents/{id}/download renders the invoice PDF on the server. Rendering is CPU-bound, so it runs in a small pool of background workers with a bounded queue. Under heavy load the queue can fill up. When that happens the API does not make you wait or time out. It responds immediately with 429 and the error code WAIT_PDF_QUEUE, telling you to retry shortly.
This is different from the quota rate limits. Both return 429, but they mean different things:
WAIT_PDF_QUEUE |
Quota rate limit | |
|---|---|---|
| Cause | server render pool momentarily saturated | your app exceeded its per-endpoint quota |
message |
WAIT_PDF_QUEUE |
a generic rate-limit message |
| Lasts | seconds (the queue drains fast) | until your window resets |
| What to do | retry after Retry-After, no need to slow your overall rate |
back off, slow down, respect the cooldown |
Branch on the message field (the error code), never on the HTTP status alone, so you can tell the two apart.
What you get back
HTTP/1.1 429 Too Many Requests
Retry-After: 5
Content-Type: application/json
{
"success": false,
"data": null,
"message": "WAIT_PDF_QUEUE",
"executionTime": 2
}
The Retry-After header carries the suggested wait in seconds (typically 5). Honor it: the render slot frees up quickly and a retry almost always succeeds on the next pass.
How to handle it
The download is a read, so retrying is always safe. No Idempotency-Key is needed. Keep retrying while the code is WAIT_PDF_QUEUE, waiting for Retry-After between attempts:
async function downloadInvoicePdf(documentId: string, token: string): Promise<Blob> {
const url = `https://prod.api.fakturnia.pl/external/v1/documents/${documentId}/download`;
for (let attempt = 0; attempt < 10; attempt++) {
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
if (res.ok) return res.blob();
if (res.status === 429) {
const body = await res.clone().json().catch(() => null);
// Transient render-queue backpressure: wait the suggested time and retry.
if (body?.message === 'WAIT_PDF_QUEUE') {
const retryAfter = Number(res.headers.get('Retry-After')) || 5;
await new Promise(r => setTimeout(r, retryAfter * 1000));
continue;
}
}
// Any other status (including quota 429) is a real error for this call.
throw new Error(`Download failed: ${res.status}`);
}
throw new Error('PDF still not ready after several retries');
}
Show the wait to your users. In the Fakturnia web and mobile apps a small "preparing your file" dialog appears on
WAIT_PDF_QUEUEand keeps retrying in the background until the PDF arrives. A short, honest progress indicator beats a spinner that looks frozen.
Why a queue instead of just waiting
Holding the connection open while a long render finishes would tie up server resources and risk client timeouts under load. A bounded queue with fast 429 feedback keeps the API responsive for everyone: the rest of the API never slows down because a burst of downloads is in progress, and your client gets a clear, retryable signal instead of an opaque timeout. See Errors for the full error envelope and Rate limits for quota behavior.