PDF generation in FastAPI
Async all the way down: httpx streams the response and `StreamingResponse` hands it to the client, so a long render never blocks the event loop and never buffers.
The code
import os
import httpx
from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse
app = FastAPI()
client = httpx.AsyncClient(timeout=60.0)
@app.get("/invoices/{invoice_id}.pdf")
async def invoice_pdf(invoice_id: str):
html = await render_invoice_html(invoice_id)
request = client.build_request(
"POST",
"https://api.pdfcraft.dev/v1/render",
headers={"Authorization": f"Bearer {os.environ['PDFCRAFT_API_KEY']}"},
json={"html": html, "options": {"printBackground": True}},
)
upstream = await client.send(request, stream=True)
if upstream.status_code != 200:
await upstream.aread()
raise HTTPException(502, upstream.json()["error"]["message"])
return StreamingResponse(
upstream.aiter_bytes(),
media_type="application/pdf",
headers={"Content-Disposition": f'attachment; filename="invoice-{invoice_id}.pdf"'},
background=BackgroundTask(upstream.aclose),
)What to watch for
- The `background=BackgroundTask(upstream.aclose)` is load-bearing. Without it the upstream response is never closed and the connection pool leaks until the process runs out.
- One module-level `AsyncClient`, closed on shutdown. A client per request means a new TLS handshake every time.
- On a streamed response you must `await upstream.aread()` before touching `.json()`, or the body has not been fetched yet.
Why not run the browser yourself
You can. Puppeteer and Playwright both work, and for a script you run by hand they are the right answer. The cost arrives at the point it becomes a service someone depends on: one long-lived browser per process rather than one per request, a semaphore so four renders do not become forty, a hard timeout that force-closes a hung page before it holds a slot forever, a scheduled restart because Chromium leaks, and a Chromium version you are now responsible for patching.
None of that is hard. All of it is work that has nothing to do with your product, and it fails under concurrency rather than in testing — which is the worst time to discover it.
Other frameworks
- PDF generation in Next.js
- PDF generation in Django
- PDF generation in Ruby on Rails
- PDF generation in Laravel
- PDF generation in AWS Lambda
- PDF generation in Cloudflare Workers
- PDF generation in Supabase Edge Functions
- PDF generation in Spring Boot
- PDF generation in Express
- PDF generation in ASP.NET Core
- PDF generation in n8n