Architecture7 min read · June 9, 2026

Building a PDF Generation Microservice: The REST API Approach

Learn how to design a PDF generation microservice using a hosted REST API. Covers architecture, queueing, storage, and error handling.

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

app/Jobs/GenerateInvoicePdf.phpphp
<?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.

retry configphp
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.