We built a serverless PDF generator using @sparticuz/chromium and Puppeteer that fits in Vercel's 50MB function limit and generates 4-page proposals in 5.8 seconds at $0.004 per document.
5.8 sec
p50 generation time
8.2 sec
p99 generation time
~$0.004
Cost per document
100%
Font rendering consistency (post data-URI fix)
CHAPTER 01
Avo's sales motion required generating proposal and audit PDFs on demand. A prospect books a discovery call, the system pulls their company data, and within seconds a polished PDF brief lands in their inbox. The PDF needed to include dynamic content, consistent typography, and controlled layout. It also needed to generate in a serverless environment where installing native binaries is not an option.
Hosted PDF generation services were ruled out by the build-don't-rent mandate. Consumption of a managed API at per-document pricing compounds into a meaningful cost at 1,000 documents per month and creates a hard external dependency in the sales pipeline. React-PDF was evaluated but its subset of CSS support did not handle the layout without extensive rework of the design system. Puppeteer with @sparticuz/chromium was the correct choice: it runs a real Chromium instance in a serverless function, renders an HTML/CSS document, and exports it via page.pdf(). The challenge was that a full Chromium binary exceeds Vercel's function size limit of 50 MB. The @sparticuz/chromium package is a Lambda/Vercel-compatible Chromium build compressed with Brotli, under 50 MB.
CHAPTER 02
The HTML templates were standard Next.js server-rendered pages served from internal routes under /api/pdf-templates/[type]. The Puppeteer function navigated to the internal template URL with query parameters encoding the document content. The template route returned an HTML page with all styles inlined and no external font requests, because fonts were base64-encoded as data URIs in the CSS. No JavaScript execution was required on the rendered page.
The PDF generation function ran on a Vercel Serverless Function with a 60-second timeout. The p99 generation time for a 4-page proposal PDF was 8.2 seconds, well within the timeout. The 60-second ceiling existed to handle cold-start decompression of Chromium, which added 3 to 5 seconds on the first invocation.
ARCHITECTURE OVERVIEW
INGEST
puppeteer-core
FEATURES
@sparticuz/chromium
TRAIN
Next.js 15
v1 / v2 / v3
SERVE
Vercel Blob
Production predictions feed back into training set. Continuous retraining cadence
CHAPTER 03
The core function sequence: validate request and authenticate with Clerk; fetch company data from ClickHouse; construct the template URL; launch Chromium via chromium.executablePath(); open a Puppeteer Browser instance; navigate to the template URL; generate PDF; close the browser; upload PDF buffer to Vercel Blob storage; return signed Blob URL with 7-day expiry; trigger Resend email with the Blob URL.
A significant constraint was that the internal template URL required resolution. In serverless deployments, localhost:3000 is not a reliable address because the function runs isolated from the Next.js server. The fix was to construct the template URL using the Vercel deployment URL from the VERCEL_URL environment variable.
Memory configuration: the default Vercel Serverless Function memory allocation of 1,024 MB was insufficient for Chromium at peak. Functions exceeded memory on documents with large signal tables. The function was reconfigured to 3,008 MB. Peak resident set size during PDF generation was 1,840 MB.
Font handling: the first version fetched Geist fonts from Google Fonts CDN at render time. Puppeteer observed the networkidle wait condition as satisfied before fonts had loaded, because font requests fired asynchronously after CSS parse. The result was fallback system font in 40 to 60% of PDFs. The fix inlined all fonts as base64 data URIs in a style block inside the template.
TECH STACK
CHAPTER 04
214 documents were generated across proposal and audit categories in the first 30 days. Total infrastructure cost for PDF generation was under $1 per month in Vercel invocation compute plus Vercel Blob storage at negligible scale. This compared favorably to managed PDF services, which would have priced at $0.10 to $0.25 per document.
5.8 sec
p50 generation time
8.2 sec
p99 generation time
~$0.004
Cost per document
100%
Font rendering consistency (post data-URI fix)
CHAPTER 05
DECISION · 01
Inline fonts as data URIs. Network font requests during headless rendering are unreliable. The networkidle wait condition does not reliably cover asynchronous font fetches. Inlining eliminates the entire class of font-miss failures with a one-time template change.
DECISION · 02
Size the function's memory ahead of time. OOM errors in serverless PDF functions produce opaque failures that look like timeouts at the API layer. Profiling memory usage during local testing and setting the function allocation to 1.5x observed peak prevented the class of production failures entirely.
DECISION · 03
Template as a real page, not a string. Assembling HTML via string concatenation for Puppeteer consumption is fast to build and slow to maintain. Refactoring the template to a real Next.js route that can be rendered and inspected in a browser was worth the two hours of refactoring.
START A PROJECT
We build fast. Most projects ship in under two weeks. Start with a free 30-minute discovery call.
Start a ProjectWe rebuilt the signal scoring pipeline from scratch, fixing look-ahead contamination and adding a top-decile filter that produced 72.2% win rate on selected signals.
72.2% Win rate (top-decile signals)
Read case study →
AI / Machine LearningWe found a 50-percentage-point win rate spread between market regimes, fixed a regime classifier that was routing by symbol name instead of market structure, and built a live suppression system for anti-patterns.
62.1% Win rate in choppy regime
Read case study →
AI / Machine LearningWe built a Rust correlation engine processing 1,200 symbols with incremental sliding window updates at 340ms p95 per cycle, 14x faster than full recompute.
1,200 Symbols in correlation matrix
Read case study →