How to Render 1,000 PDF Files with jsreport and jsreport Chrome PDF
A measured, reliable workflow for rendering 1,000 separate PDFs with jsreport Chrome PDF, including concurrency, profiling, tuning and retries.
Direct answer: render one representative report first, then submit the 1,000 report requests through a bounded queue. In jsreport, worker count controls how many requests are processed concurrently; requests above that limit wait in the queue. More workers can increase batch throughput, but they do not make one report render faster. Measure the real template, data, jsreport version and machine before choosing a worker count.
This guide assumes 1,000 report requests that produce 1,000 separate PDF files. That is different from creating one 1,000-page PDF. A historical jsreport benchmark submitted 1,000 parallel requests, but each request contained a 100-invoice, 100-page report. Its results cannot predict a current batch of 1,000 small reports.
1. Prepare and validate one report
Start with a template using the chrome-pdf recipe. Validate the output manually before launching a batch:
- Paper format or explicit width and height
- Margins and page ranges
- Print or screen media behavior
- Headers, footers and page numbers
- Fonts and external assets
- JavaScript that must finish before printing
- Network requests and delayed content
- Images, charts and other large resources
The recipe supports settings including format, pageRanges, dimensions, margins, mediaType, waitForJS and waitForNetworkIdle. Settings can be stored on the template or supplied through the template’s Chrome configuration. Match the names and defaults to your installed jsreport release; the jsreport Chrome PDF documentation can change between versions.
Example Chrome PDF configuration
{
"recipe": "chrome-pdf",
"chrome": {
"format": "A4",
"marginTop": "18mm",
"marginRight": "14mm",
"marginBottom": "18mm",
"marginLeft": "14mm",
"mediaType": "print",
"waitForNetworkIdle": true
}
}
This is an illustrative template fragment, not a universal API request. Confirm the exact property names accepted by your jsreport version and test it in Studio.
2. Decide what counts as a completed file
Give every input a stable identifier before submitting it. Use that identifier in the output filename and in your job record.
reports/
invoice-000001.pdf
invoice-000002.pdf
invoice-000003.pdf
Record at least:
- Input identifier and output path
- Submission time and completion time
- Attempt number
- HTTP or jsreport status
- Output byte count
- Error text and a retry decision
Do not treat a successful HTTP response alone as proof of a valid document. Check that the file exists, is non-empty and can be opened by your PDF validation step.
3. Use bounded concurrency
Each worker processes one request at a time. If all workers are busy, additional requests wait. Begin with a modest client-side concurrency, observe the host, and increase it only when CPU, memory, queue time and error rate remain acceptable.
| Measurement | What it tells you |
|---|---|
| Active workers | How many renders are running at once |
| Queue wait time | Whether requests are arriving faster than workers can process them |
| Render time | Template, data, browser and asset cost for one report |
| CPU usage | Whether Chrome rendering is compute-bound |
| Peak memory | Whether more workers risk swapping or container limits |
| Failure rate | Whether higher concurrency reduces reliability |
| Output size | Whether images or embedded data dominate work and storage |
A simple queue controller
The following Python example shows the control logic. Replace render_one with the HTTP call or SDK operation used by your jsreport deployment. It intentionally does not claim a universal endpoint because jsreport deployments and authentication arrangements differ.
import asyncio
from pathlib import Path
CONCURRENCY = 8
OUTPUT_DIR = Path("reports")
async def render_one(report):
"""Call your jsreport integration here and return PDF bytes."""
raise NotImplementedError("Connect this function to your jsreport deployment")
async def render_and_save(report, semaphore):
async with semaphore:
data = await render_one(report)
if not data or not data.startswith(b"%PDF"):
raise ValueError(f"Invalid PDF for {report['id']}")
path = OUTPUT_DIR / f"{report['id']}.pdf"
path.write_bytes(data)
return report["id"], len(data)
async def main(reports):
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
semaphore = asyncio.Semaphore(CONCURRENCY)
tasks = [render_and_save(report, semaphore) for report in reports]
results = await asyncio.gather(*tasks, return_exceptions=True)
for report, result in zip(reports, results):
if isinstance(result, Exception):
print(f"FAILED {report['id']}: {result}")
else:
print(f"OK {result[0]}: {result[1]} bytes")
if __name__ == "__main__":
reports = [{"id": f"invoice-{i:06d}"} for i in range(1, 1001)]
asyncio.run(main(reports))
The semaphore limits in-flight requests while gather lets successful jobs finish independently. In production, persist state outside the process so a restart does not lose completed work.
4. Profile before adding workers
jsreport’s guidance is explicit: “First, make sure the chrome-pdf recipe is the bottleneck by checking the studio profile tab.” If the profile shows that template rendering, data preparation or another recipe consumes most of the time, adding Chrome workers will not solve the bottleneck.
Inspect expensive content one piece at a time:
- Render the template with representative data and no images.
- Add images at their actual dimensions and formats.
- Add charts, tables and client-side JavaScript.
- Measure long reports separately from short reports.
- Compare local and container CPU and memory limits.
Image data can make the PDF large even when CSS displays the image at a small size. For very long reports, splitting the report into smaller parts and combining them can be worth evaluating. Measure output correctness after any split because page numbering, headers and cross-page layout can change.
5. Tune Chrome allocation carefully
The current recipe documentation describes reusable Chrome instances allocated per worker thread, along with alternate allocation strategies. It also discusses increasing Chrome instances per thread for nested-report workloads, using a dedicated-process strategy, and connecting to an existing remote Chrome instance.
These are configuration choices, not automatic accelerators. Starting a Chrome process has startup cost; the documentation gives roughly 100 ms as an example cost. Reusing a process may help many small reports, while isolation can help workloads that leak memory or contain conflicting browser state. Verify behavior against your installed release and compare:
- One reusable instance per worker
- More instances per worker for nested reports
- Dedicated Chrome processes
- An existing remote Chrome service
Change one variable at a time and keep the report data fixed.
6. Run a representative benchmark
Use a sample that reflects the real batch. Include short and long reports, typical and worst-case data, real image sizes, external fonts, JavaScript and the intended container limits.
- Render 20 to 50 reports sequentially to establish single-request time.
- Repeat with a small concurrency such as 2, 4 or 8.
- Increase concurrency only while throughput improves and failures remain acceptable.
- Repeat the winning configuration after clearing or warming any caches your deployment uses.
- Run a larger soak test to expose memory growth and intermittent failures.
Report sample size, page count, output size, jsreport version, worker setting, Chrome allocation mode, CPU, memory and container limits. Without those details, a completion-time claim is not transferable.
7. What the historical benchmark does—and does not—tell you
A published jsreport article reported 641 PDF pages per second, with approximately 1,000 parallel requests completed in 156 seconds and reported memory consumption of 1.5 GB. Each request rendered a 100-invoice, 100-page report with a 250 KB PDF output. The machine was an Intel Core i7-2600K at 3.4 GHz with four cores and 16 GB RAM. The figures are historical, workload-specific and approximately from 2014; they are not a modern capacity guarantee and do not measure 1,000 separate one-page files. See the original jsreport performance article for its test description.
8. Make the batch restartable
- Write each successful PDF to a temporary filename, then rename it atomically.
- Keep a durable status record keyed by report ID.
- Retry only transient failures, with a maximum attempt count and backoff.
- Do not retry invalid template data indefinitely.
- Separate timeout, browser crash, authentication, input and storage errors.
- Keep failed inputs and error details for later replay.
- Verify the final count: 1,000 requested, 1,000 terminal statuses, and the expected number of valid files.
9. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Batch gets slower as workers increase | CPU or memory contention | Reduce concurrency, inspect profiling data and compare peak memory. |
| Requests wait for a long time | All workers are occupied | Measure render time; increase workers only if host capacity allows it. |
| Blank or incomplete pages | JavaScript or network content is not ready | Use the appropriate wait setting and verify the page in Studio. |
| Images missing | Asset URL, credentials, timing or blocked network request | Check the asset request from the render environment and wait for required content. |
| Fonts differ between environments | Font unavailable in the server or container | Install or package the required fonts and render with the same image used in production. |
| Out-of-memory errors | Too many concurrent browsers, large images or long documents | Lower concurrency, reduce asset size, split long reports and inspect Chrome allocation. |
| Duplicate files after restart | Work state was held only in memory | Persist job status and make output writes idempotent. |
| One report is slow while others are normal | Its data, images or page count is unusually large | Profile that report separately; do not raise global concurrency to fix it. |
| Behavior differs after upgrade | Recipe defaults or option names changed | Read the documentation for the installed version and rerun the representative benchmark. |
10. Cost, performance and reliability notes
Your main capacity variables are report complexity, page count, image bytes, JavaScript, network waits, worker count, Chrome reuse and host limits. Storage also matters: 1,000 PDFs can consume substantially more space than their page count suggests when images are embedded.
Do not buy hardware from the historical benchmark alone. Size the server from your measured peak CPU, peak memory, queue time, failure rate and required completion window. If the batch is business-critical, reserve capacity for retries and leave headroom for occasional large reports.
Or skip the browser setup
If your goal is clean screenshots or PDFs from web pages rather than templated business reports, ScreenshotNeo provides a single HTTP request. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets. Bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server lets Claude, Cursor and other MCP clients use take_screenshot, get_page_info and capture_pdf.
See the ScreenshotNeo API documentation for the available options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo includes full-page capture, element capture, PDF settings, custom CSS and JavaScript, waits, request blocking, cookies, headers, geolocation, caching, signed links, async jobs, bulk capture and a usage API. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
FAQ
Can I guarantee that 1,000 PDFs finish in a specific time?
No. The result depends on the report, data, assets, jsreport version, worker setting and hardware. Benchmark your representative workload.
Does increasing workers speed up one PDF?
No. Workers increase request concurrency. A single report still uses one worker at a time.
Should every report start a new Chrome process?
Not by default. Process startup has a cost, and reuse may be faster for many small reports. Compare allocation strategies with your workload.
Is the historical 641-pages-per-second result a target?
No. It describes an old, specific machine and a different workload. Treat it as context, not a capacity promise.
When should I split a long report?
Consider splitting when profiling shows long pages or large sections dominate render time. Recheck layout, numbering and merged output after the change.


