When you integrate a PDF generation API into production, you inevitably encounter errors that never appeared during development. Network timeouts, rate limiting during traffic spikes, rendering failures from complex HTML — if you don't handle these properly, users end up with broken PDFs or batch jobs that silently stop midway.
This guide covers the most common error patterns in PDF APIs like FUNBREW PDF, along with practical retry strategies using exponential backoff. Code examples in curl, Python, Node.js, and PHP will help you build resilient error handling from day one.
For error handling in large-scale batch jobs, see the PDF Batch Processing Guide. API key security is covered in the Security Guide. If you are new to the API, the HTML-to-PDF Complete Guide is a good starting point, and the Quickstart by Language gets you to your first working request in minutes. For Markdown-specific error handling, see the Markdown to PDF API Guide.
Common Error Patterns
1. Timeouts (408 / 504)
Complex HTML or image-heavy templates take longer to render. Combined with network latency, this can trigger client-side or server-side timeouts.
| Cause | Solution |
|---|---|
| Slow external resource loading (images, fonts) | Inline with Base64 or use a CDN |
| Complex CSS (gradients, shadow DOM) | Simplify layout |
| Client timeout set too short | Set timeout to 60–120 seconds |
2. Rate Limiting (429 Too Many Requests)
Sending too many requests in a short window causes the API to return 429. If the response includes a Retry-After header, use that value as the wait time.
HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1711900800
3. Invalid HTML (400 Bad Request)
Malformed HTML causes rendering engine crashes or degraded output quality.
- Unclosed tags (
<div>without matching</div>) - Invalid Base64-encoded images
- References to external CSS that returns 404
For CSS-related rendering problems, see PDF-Specific CSS Layout Tips. If you are migrating from wkhtmltopdf and seeing different rendering behaviour, the wkhtmltopdf vs Chromium Comparison explains the key differences to watch out for. For general output troubleshooting, see the Troubleshooting Guide.
4. Authentication Errors (401 / 403)
These occur when the API key is invalid, expired, or lacks the required scope. Retrying won't fix this — alert immediately and rotate the key.
5. Server Errors (500 / 502 / 503)
Transient server-side issues. Treat as retryable and apply exponential backoff.
Retry Strategy: Exponential Backoff Basics
Retrying at a fixed interval concentrates load on the server. Exponential backoff increases the wait time exponentially and adds random jitter to spread out requests.
wait_time = min(initial_delay × 2^attempt + random_jitter, max_delay)
| Attempt | Example wait time (initial=1s, max=60s) |
|---|---|
| 1st | 1–3 seconds |
| 2nd | 2–6 seconds |
| 3rd | 4–12 seconds |
| 4th | 8–24 seconds |
| 5th | 16–48 seconds |
Which Errors to Retry
| Status Code | Retry? | Reason |
|---|---|---|
| 408, 429, 500, 502, 503, 504 | Yes | Transient issues |
| 400 | No | Request itself is malformed |
| 401, 403 | No | Auth issues won't be resolved by retrying |
| 404 | No | Resource does not exist |
Code Examples
curl (Shell Script)
#!/bin/bash
API_URL="https://pdf.funbrew.cloud/api/v1/generate"
API_KEY="your-api-key"
MAX_RETRIES=5
INITIAL_DELAY=1
generate_pdf_with_retry() {
local payload="$1"
local attempt=0
local delay=$INITIAL_DELAY
while [ $attempt -le $MAX_RETRIES ]; do
response=$(curl -s -w "\n%{http_code}" \
-X POST "$API_URL" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d "$payload" \
--max-time 120)
http_code=$(echo "$response" | tail -1)
body=$(echo "$response" | head -n -1)
case $http_code in
200)
echo "$body"
return 0
;;
400|401|403|404)
echo "Non-retryable error: $http_code" >&2
echo "$body" >&2
return 1
;;
408|429|500|502|503|504)
attempt=$((attempt + 1))
if [ $attempt -gt $MAX_RETRIES ]; then
echo "Max retries reached" >&2
return 1
fi
jitter=$((RANDOM % 1000))
wait_ms=$(( delay * 1000 + jitter ))
wait_sec=$(echo "scale=3; $wait_ms / 1000" | bc)
echo "Retry $attempt/$MAX_RETRIES: waiting ${wait_sec}s (HTTP $http_code)" >&2
sleep "$wait_sec"
delay=$((delay * 2))
[ $delay -gt 60 ] && delay=60
;;
esac
done
}
PAYLOAD='{"html": "<h1>Invoice</h1>", "options": {"format": "A4"}}'
generate_pdf_with_retry "$PAYLOAD"
Python
import time
import random
import requests
from dataclasses import dataclass
from typing import Optional
@dataclass
class RetryConfig:
max_retries: int = 5
initial_delay: float = 1.0
max_delay: float = 60.0
backoff_multiplier: float = 2.0
jitter_range: float = 1.0
RETRYABLE_STATUS_CODES = {408, 429, 500, 502, 503, 504}
def generate_pdf_with_retry(
html: str,
api_key: str,
options: Optional[dict] = None,
config: Optional[RetryConfig] = None,
) -> bytes:
"""Generate a PDF with exponential backoff retry."""
if config is None:
config = RetryConfig()
url = "https://pdf.funbrew.cloud/api/v1/generate"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
payload = {"html": html, "options": options or {"format": "A4"}}
last_exception = None
delay = config.initial_delay
for attempt in range(config.max_retries + 1):
try:
response = requests.post(url, json=payload, headers=headers, timeout=120)
if response.status_code == 200:
return response.content
if response.status_code not in RETRYABLE_STATUS_CODES:
error_info = response.json() if response.content else {}
raise ValueError(
f"Non-retryable error: HTTP {response.status_code} - {error_info}"
)
# Honour Retry-After for rate limiting
if response.status_code == 429:
retry_after = response.headers.get("Retry-After")
if retry_after:
delay = float(retry_after)
except requests.exceptions.Timeout as e:
last_exception = e
except requests.exceptions.ConnectionError as e:
last_exception = e
if attempt >= config.max_retries:
break
jitter = random.uniform(0, config.jitter_range)
wait_time = min(delay + jitter, config.max_delay)
print(f"Retry {attempt + 1}/{config.max_retries}: waiting {wait_time:.2f}s")
time.sleep(wait_time)
delay = min(delay * config.backoff_multiplier, config.max_delay)
raise RuntimeError(
f"Max retries ({config.max_retries}) exceeded"
) from last_exception
# Usage
try:
pdf_bytes = generate_pdf_with_retry(
html="<h1>Invoice</h1><p>Total: $100.00</p>",
api_key="your-api-key",
options={"format": "A4", "margin": {"top": "20mm"}},
)
with open("invoice.pdf", "wb") as f:
f.write(pdf_bytes)
print("PDF generated successfully")
except ValueError as e:
print(f"Input error (fix required): {e}")
except RuntimeError as e:
print(f"Server error (retry later): {e}")
Node.js
const axios = require('axios');
const RETRYABLE_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504]);
/**
* Generate a PDF with exponential backoff retry.
*/
async function generatePdfWithRetry(html, apiKey, options = {}, retryConfig = {}) {
const {
maxRetries = 5,
initialDelay = 1000, // milliseconds
maxDelay = 60000,
backoffMultiplier = 2,
} = retryConfig;
let delay = initialDelay;
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await axios.post(
'https://pdf.funbrew.cloud/api/v1/generate',
{ html, options: { format: 'A4', ...options } },
{
headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
responseType: 'arraybuffer',
timeout: 120000,
}
);
return Buffer.from(response.data);
} catch (error) {
const status = error.response?.status;
if (status && !RETRYABLE_STATUS_CODES.has(status)) {
const errorBody = error.response?.data
? JSON.parse(Buffer.from(error.response.data).toString())
: {};
throw new Error(`Non-retryable error: HTTP ${status} - ${JSON.stringify(errorBody)}`);
}
lastError = error;
if (attempt >= maxRetries) break;
let waitTime = delay;
if (status === 429) {
const retryAfter = error.response?.headers?.['retry-after'];
if (retryAfter) waitTime = parseFloat(retryAfter) * 1000;
}
const jitter = Math.random() * 1000;
const actualWait = Math.min(waitTime + jitter, maxDelay);
console.warn(`Retry ${attempt + 1}/${maxRetries}: waiting ${(actualWait / 1000).toFixed(2)}s`);
await new Promise((resolve) => setTimeout(resolve, actualWait));
delay = Math.min(delay * backoffMultiplier, maxDelay);
}
}
throw new Error(`Max retries (${maxRetries}) exceeded`, { cause: lastError });
}
// Usage
generatePdfWithRetry(
'<h1>Invoice</h1><p>Total: $100.00</p>',
'your-api-key',
{ format: 'A4' },
{ maxRetries: 3, initialDelay: 2000 }
)
.then((buf) => {
require('fs').writeFileSync('invoice.pdf', buf);
console.log('PDF generated successfully');
})
.catch((err) => console.error('Error:', err.message));
PHP
<?php
class PdfApiClient
{
private const RETRYABLE_STATUS_CODES = [408, 429, 500, 502, 503, 504];
public function __construct(
private string $apiKey,
private string $baseUrl = 'https://pdf.funbrew.cloud/api/v1',
private int $maxRetries = 5,
private float $initialDelay = 1.0,
private float $maxDelay = 60.0,
private float $backoffMultiplier = 2.0,
) {}
/**
* Generate a PDF with exponential backoff retry.
*
* @throws \RuntimeException When max retries are exhausted
* @throws \InvalidArgumentException For non-retryable errors
*/
public function generateWithRetry(string $html, array $options = []): string
{
$delay = $this->initialDelay;
$lastError = null;
for ($attempt = 0; $attempt <= $this->maxRetries; $attempt++) {
try {
return $this->callApi($html, $options);
} catch (\RuntimeException $e) {
$statusCode = $e->getCode();
if (!in_array($statusCode, self::RETRYABLE_STATUS_CODES, true)) {
throw new \InvalidArgumentException(
"Non-retryable error: HTTP {$statusCode} - " . $e->getMessage(),
$statusCode
);
}
$lastError = $e;
if ($attempt >= $this->maxRetries) break;
$jitter = mt_rand(0, 1000) / 1000;
$waitTime = min($delay + $jitter, $this->maxDelay);
error_log("Retry " . ($attempt + 1) . "/{$this->maxRetries}: waiting {$waitTime}s");
usleep((int) ($waitTime * 1_000_000));
$delay = min($delay * $this->backoffMultiplier, $this->maxDelay);
}
}
throw new \RuntimeException(
"Max retries ({$this->maxRetries}) exceeded: " . $lastError?->getMessage(),
0,
$lastError
);
}
private function callApi(string $html, array $options): string
{
$ch = curl_init("{$this->baseUrl}/generate");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode([
'html' => $html,
'options' => array_merge(['format' => 'A4'], $options),
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer {$this->apiKey}",
'Content-Type: application/json',
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 120,
]);
$body = curl_exec($ch);
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($curlError) {
throw new \RuntimeException("cURL error: {$curlError}", 0);
}
if ($statusCode === 200) {
return $body;
}
throw new \RuntimeException(
json_decode($body, true)['message'] ?? 'Unknown error',
$statusCode
);
}
}
// Usage
$client = new PdfApiClient(apiKey: 'your-api-key');
try {
$pdf = $client->generateWithRetry(
html: '<h1>Invoice</h1><p>Total: $100.00</p>',
options: ['format' => 'A4']
);
file_put_contents('invoice.pdf', $pdf);
echo "PDF generated successfully\n";
} catch (\InvalidArgumentException $e) {
echo "Input error (fix required): " . $e->getMessage() . "\n";
} catch (\RuntimeException $e) {
echo "Server error (retry later): " . $e->getMessage() . "\n";
}
Error Logging Best Practices
Good logging makes it easy to detect problems early and pinpoint root causes.
What to Include in Logs
{
"timestamp": "2026-03-31T12:00:00Z",
"level": "error",
"event": "pdf_generation_failed",
"request_id": "req_abc123",
"attempt": 3,
"max_retries": 5,
"status_code": 503,
"error_message": "Service Unavailable",
"html_size_bytes": 15420,
"duration_ms": 5230,
"will_retry": true
}
Log Level Guidelines
| Situation | Log Level | Action |
|---|---|---|
| Retryable error (in progress) | warn |
No alert needed |
| Max retries reached | error |
Trigger alert |
| Auth error (401/403) | error |
Immediate alert |
| Input error (400) | warn |
Code fix needed |
| Success (after retries) | info |
Record only |
FUNBREW PDF-Specific Tips
Once your retry logic is in place, consider adding Webhook notifications so your system is alerted the moment a generation job fails — see the Webhook Integration Guide. For real-world error handling patterns in production workflows, the Invoice PDF Automation Guide and Certificate PDF Automation Guide both include failure-recovery strategies. For Next.js/Nuxt-specific error handling patterns, see the Next.js & Nuxt PDF API Guide.
Handling Timeouts
If your HTML references external resources, inline them as Base64 to eliminate load time.
{
"html": "<img src='data:image/png;base64,iVBORw0KGgo...' />",
"options": {
"format": "A4",
"waitUntil": "networkidle0"
}
}
Avoiding Rate Limits
Use batch processing to generate multiple PDFs in a single request, reducing your total API call count.
{
"batch": [
{ "html": "<h1>Invoice #001</h1>", "filename": "invoice-001.pdf" },
{ "html": "<h1>Invoice #002</h1>", "filename": "invoice-002.pdf" }
]
}
Rotating API Keys
If authentication errors persist, regenerate your API key from the dashboard. Always use separate keys for production and staging. Rate limits vary by plan, so if you are hitting 429s frequently, check the Pricing Comparison to find a plan that fits your volume.
Browse real-world automation examples — invoices at /use-cases/invoices, reports at /use-cases/reports, and more at the full Use Cases gallery.
Summary
Key takeaways for robust PDF API error handling:
- Classify errors: Distinguish retryable (some 4xx, all 5xx) from non-retryable (400, 401, 403)
- Use exponential backoff: Increase wait time exponentially, add jitter
- Set limits: Always define max retries and max wait time
- Log everything: Record request ID, status code, and retry count
- Alert on critical failures: Max retries reached and auth errors should page someone
For the full production readiness picture — including error handling, monitoring, and scaling — see the PDF API Production Checklist. Check the FUNBREW PDF API reference for the full list of error codes and recommended responses. Use the playground to test your HTML before integrating. To see certificate or invoice generation in action, visit the Use Cases gallery. For serverless PDF generation on AWS Lambda, see the Serverless PDF Guide. Django/FastAPI integration is covered in the Django/FastAPI Guide. For migration-specific error patterns, see the Puppeteer Migration Guide. Report generation error handling is in the Report PDF Generation Guide. Compare PDF API error handling approaches in the PDF API Comparison.