What Is a PDF Service?
A PDF service generates portable document files on demand from structured templates, handles digital signature embedding for legal and financial documents, and manages asynchronous job queues so that caller services are not blocked waiting for potentially slow rendering operations. It acts as a centralized rendering layer that decouples document generation from application logic.
Requirements
Functional Requirements
- Accept a template identifier and a JSON data payload; render the template and return a PDF.
- Support synchronous rendering for small documents (under 5 pages) and async job submission for large or complex documents.
- Embed digital signatures using X.509 certificates for documents requiring legal validity.
- Provide a secure, time-limited download URL for completed async jobs.
- Store rendered PDFs for a configurable retention period (default 30 days).
Non-Functional Requirements
- Synchronous rendering must complete in under 3 seconds for 95% of requests.
- Async jobs must complete within 60 seconds for documents up to 200 pages.
- Download URLs must expire after a configurable TTL (default 15 minutes).
- The service must handle 1,000 concurrent async jobs.
Data Model
Template
- template_id (UUID)
- name, version (integer, incremented on update)
- html_source (stored in object storage; this field holds the S3 key)
- schema (JSONB: JSON Schema definition of accepted data fields)
- signing_required (boolean), certificate_id (optional foreign key)
RenderJob
- job_id (UUID)
- template_id, template_version
- input_data (JSONB snapshot of the caller-supplied payload)
- status (ENUM: queued, rendering, signing, completed, failed)
- output_key (S3 key of the finished PDF)
- created_at, completed_at, expires_at
Core Algorithms
HTML-to-PDF Rendering
Templates are authored in HTML with CSS for layout and a lightweight templating engine (Handlebars or Jinja2) for variable substitution. The rendering pipeline follows these steps:
- Fetch the HTML template from object storage (cached in the renderer process for 5 minutes).
- Merge the caller-supplied data into the template using the templating engine.
- Pass the resulting HTML to a headless Chromium instance via the Chrome DevTools Protocol.
- Invoke the Page.printToPDF CDP command with paper size, margin, and scale settings.
- Stream the resulting PDF bytes to object storage and record the output key in the RenderJob row.
Digital Signature Embedding
When signing_required is true, the rendered PDF passes through a signing step before the job is marked complete. The service uses a PDF signing library (e.g. iText or PDFBox) that:
- Computes a SHA-256 digest of the PDF content.
- Signs the digest using the private key referenced by certificate_id, stored in a hardware security module or a secrets manager.
- Embeds the signature, certificate chain, and timestamp in a PKCS7 signature dictionary within the PDF.
Scalability
Headless Chromium processes are expensive to spawn. Each renderer node maintains a warm pool of 4 browser contexts, accepting jobs from a local queue. A job dispatcher distributes incoming jobs across renderer nodes using a weighted least-connections algorithm: nodes report their current queue depth via a shared Redis sorted set, and the dispatcher always routes to the lowest-depth node.
The async job queue (backed by SQS or a Postgres advisory lock queue) provides natural backpressure. If all renderer nodes are saturated, jobs wait in the queue without blocking callers. Horizontal scaling of renderer nodes is triggered when the queue depth exceeds a threshold for more than 60 seconds.
API Design
- POST /render/sync — submit template ID and data; returns PDF bytes inline. Use for small documents with low latency requirements.
- POST /render/async — submit a render job; returns job_id immediately.
- GET /jobs/{job_id} — poll job status; returns status and, when completed, a pre-signed download URL.
- POST /templates — create a new template version by uploading HTML source and schema.
- GET /templates/{template_id} — retrieve template metadata and current version.
Download URLs are generated as pre-signed S3 GET URLs with a 15-minute expiry. The URL is not stored in the database; it is generated on demand when the caller requests the job status of a completed job.
Failure Modes
- Chromium crash: The renderer node detects the crash via the CDP WebSocket disconnect, marks the job as failed, and re-enqueues it for retry on a different node. Maximum 3 retries.
- Signing service unavailable: The job enters a signing_retry sub-state and is retried with exponential backoff. The PDF is not exposed to the caller until signing succeeds.
- Object storage write failure: The job is marked failed; the incomplete object is cleaned up by a periodic garbage collection job that looks for orphaned keys with no corresponding completed RenderJob.
Observability
Key metrics: render job throughput (jobs/sec), p95 render latency, queue depth, renderer pool utilization per node, signing latency, and job failure rate by failure reason. Alert when queue depth exceeds 500 jobs or p95 render latency exceeds 5 seconds.
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How does an HTML-to-PDF service handle template rendering at scale?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “The service accepts a template identifier and a JSON data payload, merges them server-side using a templating engine (e.g., Handlebars or Jinja2), and passes the resulting HTML to a headless browser (e.g., Chromium via Puppeteer) or a dedicated rendering library (e.g., WeasyPrint). Templates are cached in memory after first load to avoid repeated disk I/O.”
}
},
{
“@type”: “Question”,
“name”: “How is a digital signature embedded in a generated PDF?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “After the raw PDF is generated, the service calls a signing module that computes a cryptographic hash of the PDF byte range, signs it with the organization's private key using a standard like PKCS#7, and embeds the signature dictionary into the PDF's cross-reference table. The result is a PDF/A-conformant signed document verifiable by any standard PDF reader.”
}
},
{
“@type”: “Question”,
“name”: “Why use an async job queue with polling for PDF generation instead of a synchronous API?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “PDF rendering can take seconds for complex documents, far exceeding typical HTTP timeout budgets. An async model enqueues the job and returns a job ID immediately. The caller polls a status endpoint or receives a webhook when the job completes, allowing the rendering workers to be scaled independently and preventing client-side timeouts from causing retries that overload the system.”
}
},
{
“@type”: “Question”,
“name”: “How are secure download URLs generated for completed PDFs?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “On job completion the service stores the PDF in object storage (e.g., S3) and generates a pre-signed URL with a short TTL (e.g., 15 minutes) and a signed HMAC token tied to the requester's identity. The URL is returned in the polling response or webhook payload. After expiry, the caller must request a fresh URL, preventing unauthorized access to stale links.”
}
}
]
}
See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering
See also: Atlassian Interview Guide