As applications grow, PDF generation often becomes a bottleneck. It's CPU-intensive, slow under concurrent load, and tightly coupled to the web process. A natural solution is to extract it into a dedicated microservice.
But "microservice" doesn't have to mean "write and deploy another service." If you use a hosted REST API for PDF rendering, you already have the hard part handled. What remains is the orchestration layer — queueing, storage, and notification.
Architecture
The recommended pattern for high-volume PDF generation:
1. Web process receives a request ("generate invoice PDF for order #42"), enqueues a job, and returns 202 Accepted immediately.
2. Queue worker picks up the job, renders HTML (from a template + data), calls the PDF API, and stores the resulting bytes in object storage (S3, GCS, R2).
3. Worker updates the database with the storage URL and optionally sends a webhook or email notification.
This pattern keeps your web process fast, decouples generation from request/response cycles, and makes it trivial to scale PDF workers independently.
Laravel Queue Example
<?php
namespace App\Jobs;
use App\Models\Invoice;
use HtmlToPdfApi\Client;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Storage;
class GenerateInvoicePdf implements ShouldQueue
{
use Queueable;
public function __construct(public readonly Invoice $invoice) {}
public function handle(Client $pdf): void
{
$html = view('invoices.pdf', ['invoice' => $this->invoice])->render();
$bytes = $pdf->fromHtml($html)->paperSize('a4')->generate();
$path = "invoices/{$this->invoice->number}.pdf";
Storage::disk('s3')->put($path, $bytes, ['ContentType' => 'application/pdf']);
$this->invoice->update(['pdf_path' => $path, 'pdf_generated_at' => now()]);
}
}Error Handling and Retries
PDF APIs return structured errors. Map them to retry logic:
401 — bad API key, don't retry, alert the team.
422 — malformed HTML or missing field, don't retry, log the payload.
429 — rate limited, retry with exponential backoff.
5xx — transient server error, retry up to 3 times.
public int $tries = 3;
public int $backoff = 10; // seconds between retries
public function failed(\Throwable $e): void
{
\Log::error('PDF generation failed', [
'invoice' => $this->invoice->id,
'error' => $e->getMessage(),
]);
}Scaling
With this architecture, scaling PDF throughput means adding more queue workers — not changing any application code or managing rendering infrastructure. The PDF API handles concurrent rendering on its side; your workers just make HTTP calls.