Volver a la bitácora
17 de abril de 20266 min de lectura

Generar PDF con Puppeteer en serverless: arquitectura, costos y trampas

Cómo ReclamaAI genera miles de documentos jurídicos en PDF desde funciones serverless. Por qué Chromium pesa 50MB, cómo lo metimos en Vercel, y qué hacer cuando se cae.

stack-decisionsreclamaai

Generar PDF desde HTML suena trivial — abres una página, llamas a page.pdf(), listo. En serverless es donde se complica. El binario de Chromium pesa 50MB, supera el límite de muchas plataformas, y arrancarlo en cada invocación es lento. Esta es la arquitectura que llegamos a usar en ReclamaAI tras tres iteraciones.

Por qué no usamos un servicio externo

Lo primero que probé fue PDFShift, DocRaptor y Browserless. Todos funcionan, pero los costos crecen rápido cuando generas miles de documentos al mes, y ninguno me daba el control que necesitaba sobre tipografías, headers/footers personalizados y firmas embebidas. Decidí internalizarlo.

La arquitectura final

ReclamaAI usa @sparticuz/chromium en lugar del paquete oficial de Puppeteer. Es un build de Chromium específicamente comprimido para AWS Lambda y compatible con Vercel functions. Pesa 45MB en lugar de 280MB, lo cual nos mete justo debajo del límite de 50MB de los functions de Vercel sin pagar el plan enterprise.

La función de generación es así: recibe un HTML preparado por el motor de plantillas, lanza Chromium en modo headless, espera networkidle0 (importante para que las fuentes carguen), llama page.pdf({ format: "Letter", printBackground: true, margin: ... }), y devuelve el buffer. El buffer se sube a S3 en otra promise, no en la misma función — para mantener el tiempo total bajo control.

El cold start

La primera invocación tarda 6-8 segundos (lanzar Chromium en frío). Las siguientes, mientras la función esté caliente, tardan 1.5-2 segundos. Si el usuario está esperando frente a la pantalla, 8 segundos es eternidad.

Solución: la generación es asíncrona desde el primer día. El usuario hace clic en "generar", el frontend recibe un job_id, y un worker (BullMQ + Redis) procesa el job. El frontend hace polling cada 2 segundos. Cuando el documento está listo, mostramos preview y botón de descarga. Para el usuario, los 8 segundos del cold start se sienten como un loading state legítimo, no como una página colgada.

Tipografías: el detalle que casi me derrota

Chromium en serverless no trae fuentes del sistema. Si tu HTML usa font-family: Arial, el PDF se renderiza con la fuente de fallback de Chromium, que es horrible. Para documentos jurídicos esto es inaceptable.

La solución: empacamos las fuentes que necesitamos como base64 inline en el CSS del template, vía @font-face. Pesa más el HTML (unos 200KB extra) pero garantiza que el render sea idéntico en cualquier ambiente. Para ReclamaAI cargamos Inter (cuerpo) y Times (cita legal por convención).

Costos reales

En el plan Hobby de Vercel hay suficiente para arrancar. Cuando empiezas a rozar el límite de Function Execution Time (60s en Hobby), conviene migrar al plan de pago para tener funciones más largas. A escala de miles de documentos al mes, el costo de Vercel functions sigue siendo modesto comparado con un proveedor PDF-as-a-service externo a precio de lista. La diferencia, multiplicada por mes, paga rápido el sprint de implementación interna.

Lo que rompimos en producción

Una vez Vercel actualizó su runtime de Node y el binario de @sparticuz/chromium dejó de funcionar por una hora. Lección aprendida: tener un fallback. Ahora si Puppeteer falla 3 veces seguidas, encolamos el documento para regenerar más tarde y le mandamos al usuario un correo automático: "tu documento está en cola, te llega en menos de 30 minutos". Nunca hemos tenido que usar el fallback más de un par de veces, pero saber que existe me deja dormir.