Batch PDF Generation at Scale with Blink PDF REST API
Generate thousands of PDFs concurrently using async Python or Node.js. Learn concurrency limits by plan, exponential backoff, and throughput optimization.
Use this file to discover all available pages before exploring further.
When you need to produce hundreds or thousands of PDFs in a single run — month-end invoices, bulk certificate generation, mass document digitization — the key is concurrency. Blink PDF can render roughly 30 PDFs per second per node, and your throughput scales with the number of parallel requests you issue within your plan’s rate limit. This guide shows you how to structure a concurrent batch job in Python and Node.js, handle rate limit errors gracefully, and size your plan for the throughput you need.
Your plan’s rate limit determines how many requests you can issue per minute. For batch workloads, translate that into a practical concurrency ceiling:
Plan
Rate Limit
Recommended Max Concurrency
500 PDFs in ~…
Free
5/min
1
~100 min
Starter ($12/mo)
30/min
3
~17 min
Pro ($39/mo)
120/min
10
~4 min
Business ($149/mo)
600/min
40
~50 sec
Scale ($999/mo)
~5,000/min
per capacity
~6 sec
These concurrency figures are conservative estimates that keep you safely below the rate limit ceiling while accounting for variable render times. You can tune the concurrency value up or down based on observed throughput in your environment.
The most efficient Python approach uses asyncio with aiohttp to issue multiple render requests in parallel, capped by a semaphore set to your concurrency limit.
import asyncioimport aiohttpimport timefrom pathlib import PathBLINK_API_KEY = "sk_live_..."BLINK_URL = "https://api.blinkpdf.io/v1/render"# Set this to the recommended concurrency for your plan:# Free=1, Starter=3, Pro=10, Business=40, Scale=adjust as neededMAX_CONCURRENCY = 10 # Pro plan exampleasync def render_pdf( session: aiohttp.ClientSession, semaphore: asyncio.Semaphore, document: dict, retries: int = 4,) -> tuple[str, bytes | None]: """ Render a single PDF with exponential backoff on 429 / 5xx errors. Returns (document_id, pdf_bytes) or (document_id, None) on failure. """ async with semaphore: delay = 1.0 # initial backoff in seconds for attempt in range(retries): try: async with session.post( BLINK_URL, headers={ "Authorization": f"Bearer {BLINK_API_KEY}", "Content-Type": "application/json", }, json={ "markdown": document["markdown"], "metadata": {"title": document["title"]}, }, ) as response: if response.status == 200: pdf_bytes = await response.read() render_ms = response.headers.get("X-Render-Ms", "?") print( f" ✓ {document['id']} rendered in {render_ms}ms" ) return document["id"], pdf_bytes elif response.status == 429: # Rate limited — back off and retry print( f" ⚠ {document['id']} rate limited, " f"retrying in {delay:.1f}s (attempt {attempt + 1}/{retries})" ) await asyncio.sleep(delay) delay *= 2 # exponential backoff else: body = await response.text() print( f" ✗ {document['id']} failed with " f"{response.status}: {body[:120]}" ) return document["id"], None except aiohttp.ClientError as exc: print(f" ✗ {document['id']} network error: {exc}") await asyncio.sleep(delay) delay *= 2 print(f" ✗ {document['id']} exhausted retries") return document["id"], Noneasync def batch_render(documents: list[dict], output_dir: str = "output") -> dict: """ Render all documents concurrently and save PDFs to output_dir. Returns a summary dict with success/failure counts. """ Path(output_dir).mkdir(parents=True, exist_ok=True) semaphore = asyncio.Semaphore(MAX_CONCURRENCY) connector = aiohttp.TCPConnector(limit=MAX_CONCURRENCY) async with aiohttp.ClientSession(connector=connector) as session: tasks = [ render_pdf(session, semaphore, doc) for doc in documents ] start = time.monotonic() results = await asyncio.gather(*tasks) elapsed = time.monotonic() - start success, failure = 0, 0 for doc_id, pdf_bytes in results: if pdf_bytes is not None: out_path = Path(output_dir) / f"{doc_id}.pdf" out_path.write_bytes(pdf_bytes) success += 1 else: failure += 1 print( f"\nBatch complete: {success} succeeded, {failure} failed " f"in {elapsed:.1f}s ({len(documents) / elapsed:.1f} PDFs/sec)" ) return {"success": success, "failure": failure, "elapsed_s": elapsed}# Example: build a list of 100 documentsdocuments = [ { "id": f"report_{i:04d}", "title": f"Monthly Report — Account {i:04d}", "markdown": f"# Monthly Report\n\n**Account:** {i:04d}\n\nContent for account {i}.", } for i in range(1, 101)]asyncio.run(batch_render(documents, output_dir="reports"))
Set MAX_CONCURRENCY to the value in the plan table above and tune it upward only after confirming you are not hitting 429 errors in practice. Starting conservative saves you from failed renders at the beginning of a large run.
Node.js: Concurrent Batch with Promise.all and a Concurrency Pool
Node.js’s Promise.all runs tasks concurrently, but without a concurrency cap it will fire all requests simultaneously and overwhelm the rate limit. The renderBatch function below implements a simple pool that keeps exactly MAX_CONCURRENCY requests in flight at any time.
When you exceed your plan’s rate limit, the API returns HTTP 429 Too Many Requests. Both examples above implement exponential backoff — they wait 1 second after the first 429, 2 seconds after the second, 4 seconds after the third, and so on.Key principles for robust batch jobs:
Never retry immediately on 429. Back off before each retry attempt.
Cap your retries (4–5 attempts is usually sufficient). After that, log the failure and move on so the rest of the batch can complete.
Log failures with document IDs so you can re-run just the failed subset without re-processing the whole batch.
Monitor X-Render-Ms across your batch run — sustained render times above 200ms may indicate downstream capacity pressure.
If you consistently hit 429 errors even at the recommended concurrency levels, your workload has outgrown your current plan. Upgrading to the next plan tier immediately increases your rate limit and allows higher concurrency.
Example: You need to generate 10,000 invoices within 30 minutes.
Required rate = 10,000 / 30 ≈ 334 PDFs/min
The Business plan (600/min) covers this comfortably. The Pro plan (120/min) would require ~83 minutes.
Pro Plan — $39/mo
120 req/min · 25k PDFs/mo — Good for nightly batch jobs of a few hundred PDFs or on-demand runs of up to ~1,000 documents.
Business Plan — $149/mo
600 req/min · 200k PDFs/mo — Suitable for large month-end batch runs, bulk document digitization, and high-frequency automated pipelines.
The Scale plan ($999/mo) offers unlimited fair-use throughput and is designed for enterprise workloads that require sustained high-volume generation. Contact sales to discuss capacity planning for Scale-tier batch workloads.