Skip to main content
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.

Concurrency Limits by Plan

Your plan’s rate limit determines how many requests you can issue per minute. For batch workloads, translate that into a practical concurrency ceiling:
Rate limits count requests, while your monthly allowance is metered in render units (a text render costs 1 unit; image-heavy renders cost more, up to 16). Plan for both: requests/min bound your throughput, render units bound your monthly volume.
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.

Python: Async Batch with asyncio and aiohttp

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.
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.

Handling Rate Limit Errors

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-Status and X-Render-Diagnostics across your batch run — a rising share of degraded renders or error-severity diagnostic codes can flag documents with missing glyphs, image fetch failures, or other issues. When you need a strict match to the source Markdown, also check X-Render-Rendered-As-Requested. Log X-Request-Id for any failures so support can trace them.
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.
Pre-validate templates without spending render units or throughput: POST /v1/render/validate runs the full schema and semantic checks and has its own rate-limit bucket, separate from render. And apply a consistent theme across the batch so every document shares one look.

Shared Images: Stage Once, Reference Everywhere

Batch jobs often reuse the same logo or letterhead across every document. Instead of inlining that image as a large data: URL in every request — which counts toward your input-size limit and bloats each payload — stage it once and reference it by handle:
  1. Stage it through the MCP upload_image tool — one call for a small image (or ordered parts for a larger one) that takes the image bytes (base64) and returns the blink://asset/<sha256> handle directly (identical bytes dedupe and return a dup flag). The server hashes the bytes for you, so no client-side SHA-256 step and no presigned PUT are involved.
  2. Reference it in every render as ![Logo](blink://asset/<sha256>).
upload_image handles one image at a time, so if a batch mixes several logos, stage each distinct image before referencing it. Staged assets are fetched at render time and do not count toward the request input-size limit, they are content-addressed (identical bytes are stored once), and they are scoped per credential — so a handle staged with your batch key resolves in every POST /v1/render call that key makes. Assets live up to 24 hours, so one staging step covers a full batch run. For image-only outputs — digitizing scans or assembling photo batches — use the imageSequence field to lay staged assets out as pages (one per page or a grid) without hand-writing Markdown. See Document Options.

Sizing Your Plan for Batch Workloads

Use this formula to estimate the minimum plan you need:
Example: You need to generate 10,000 invoices within 30 minutes.
The Business plan (600/min) covers this comfortably. The Pro plan (120/min) would require ~83 minutes.

Pro Plan — $9/mo

120 req/min · 30k render units/mo — Good for nightly batch jobs of a few hundred documents or on-demand runs of up to ~1,000 text documents.

Business Plan — $79/mo

600 req/min · 1M render units/mo — Suitable for large month-end batch runs, bulk document digitization, and high-frequency automated pipelines.
The Enterprise plan offers dedicated capacity with custom rate limits and no per-render metering — designed for workloads that require sustained high-volume generation. Contact sales to discuss capacity planning.