“Make reports faster” sounds like a single problem. At enterprise volumes, it’s a system problem with four distinct bottlenecks:
- data retrieval (queries, APIs, joins, parameterization)
- layout complexity (repeat nestings, large tables, pagination)
- export/rendering (PDF engine cost under load)
- delivery (email, uploads, downstream throttling)
This playbook is about making report generation predictable under load: lower p95 latency, fewer failures, and fewer “it worked yesterday” incidents.
Define the workload before you optimize
Most reporting systems accidentally mix three workloads that need different architectures:
- Interactive preview (human in the loop): low latency, low fanout
- API export (on-demand PDF): medium latency, spiky traffic, needs backpressure
- Scheduled jobs (bulk delivery): high fanout, queue + workers, strict reliability
If you’re optimizing “report generation” without stating which workload you care about, you’ll fix the wrong thing.
CxReports supports all three patterns via:
- API PDF export endpoints (on-demand)
- scheduled delivery jobs (email / Google Drive uploads)
- embedding in apps (preview use cases)
Docs:
Measure first: a minimal performance scorecard
You don’t need a full observability program to make progress. Start with these metrics:
- p50 / p95 generation time for preview and PDF export
- queue depth / oldest job age (for scheduled workloads)
- failure rate and top error causes (timeouts, memory, data)
- cost per 1,000 PDFs (CPU time + storage + delivery)
- time-to-deliver (generation + email/upload + retries)
The goal is to turn “it feels slow” into a specific bottleneck you can attack.
Where time goes (and how to win it back)
1) Data retrieval: fewer round trips, cheaper queries
Symptoms:
- reports time out only for certain parameters
- p95 explodes when data volume grows
- “fast export, slow preview” or vice versa
High-leverage fixes:
- Batch and reduce round trips: fewer queries, fewer API calls
- Use parameters intentionally: avoid unbounded queries
- Move heavy joins upstream: aggregate once, reuse many times
- Prefer reporting-friendly stores: read replicas / warehouses for heavy reporting
CxReports supports multiple data source types (SQL, API, JSON/CSV, JavaScript, MongoDB), so you can choose the right retrieval shape per report section.
Docs: Data Sources
If your app can assemble “render-ready” data, you can push it at render time using Temporary Data and keep the report layer focused on presentation.
Docs: Temporary Data Upload
2) Layout complexity: avoid accidental O(n²)
Symptoms:
- reports are fast with small inputs but non-linear with large inputs
- exports are CPU-heavy even when data queries are fast
- huge tables dominate runtime
Fixes:
- Paginate intentionally: avoid “one page tries to contain everything”
- Reduce nested repeating components: nested repeats often multiply DOM size
- Design for printing: stable page types and templates reduce reflow surprises
If you’re generating multi-page documents, invest in reusable templates and page types so your “layout cost” stays stable as report count increases.
Related: content/multi-page-document-generation-blog-post.md
3) Delivery: email and uploads need throttling and parallelism
Symptoms:
- “report generated but not delivered”
- SMTP errors, rate limits, slow sends, angry recipients
CxReports supports:
- scheduled delivery
- email delivery settings with DegreeOfParallelism
- email throttling to avoid flooding recipients/SMTP
- jobs that upload to Google Drive as a delivery target
Docs:
Operational guidance:
- Throttle intentionally (deliver 10k emails slowly, reliably)
- Separate generation from delivery (don’t fail the PDF because SMTP is slow)
- Add idempotency in your scheduler (avoid duplicate sends on retries)
Scaling architecture: queue + workers beats request/response for bulk workloads
Here’s the mental model:
- Preview: request/response is fine
- On-demand PDF: request/response may work at low volume, but add backpressure
- Bulk generation: use a queue + worker pool + retries + dead-letter handling
If your reporting system must deliver thousands of PDFs (month-end, quarter-end), treat it like a batch processing system:
- job queue
- worker concurrency limits
- retry policies
- visibility into job status
A pragmatic 30/60/90 plan
Days 0–30: define the problem before optimizing
- inventory all reports and classify them (preview, on-demand, bulk)
- identify which reports are slow, heavy, or failure-prone
- determine where time is spent per report: data retrieval, layout, or delivery
- analyze the shape and volume of data passed into reports
The goal is clarity: what your reports are, how expensive they are, and why.
Days 31–60: restructure reports and data
- redesign reports around predictable, bounded data inputs
- move expensive joins and aggregations upstream where possible
- simplify layouts to avoid nested repeats and non-linear rendering cost
- benchmark after every change and compare against the baseline
The goal is to make report cost proportional and predictable.
Days 61–90: align execution and delivery
- run heavy reports asynchronously
- apply concurrency limits and backpressure based on report cost
- enforce idempotency to prevent duplicate exports
- load test with realistic parameters and re-benchmark
The goal is consistent performance under load, not peak speed.
Checklist (copy/paste)
- ✓ Do we have p95 generation time tracked per report family?
- ✓ Do we cap PDF export concurrency to safe limits?
- ✓ Are heavy queries off production primaries?
- ✓ Do bulk exports run through asynchronously or through automated jobs?
- ✓ Do we throttle delivery (SMTP) and handle retries without duplicates?
Next steps
If you want help optimizing your reporting workload, a good starting packet is:
- 3 slowest reports + parameters that trigger slowness
- export volume profile (daily/weekly/monthly peaks)
- current p95 and failure rate
- delivery targets (email, drive, portal download)
We can turn that into a prioritized set of fixes (quick wins + structural changes) and a rollout plan that doesn’t break reliability.