reportgen
Back to all posts

Cost Optimization for PDF Generation: Cut Your Bill by 70% Without Sacrificing Quality šŸ’°

May 16, 2025

You're generating 10,000 PDFs per month. Your bill is $500+.

You think: "That's just the cost of doing business."

Wrong.

We've helped dozens of teams cut their PDF costs by 50-70% without changing providers or sacrificing quality.

This guide shows you exactly how.


Understanding Your PDF Costs

Where Money Actually Goes

Most developers think PDF generation is expensive. It's not. What's expensive is doing it inefficiently.

Cost breakdown for typical PDF generation:

  1. Generation (40%) - CPU/memory to render HTML → PDF
  2. Storage (30%) - S3/CDN for hosting PDFs
  3. Bandwidth (20%) - Delivering PDFs to users
  4. API calls (10%) - Request overhead

The opportunity: Optimize each layer independently.


Strategy 1: Cache Aggressively

The Problem: Regenerating identical PDFs wastes money.

// āŒ Generating the same PDF repeatedly
app.get('/invoice/:id', async (req, res) => {
  const invoice = await db.invoices.findOne({ id: req.params.id });
 
  // Generates PDF every single time, even if nothing changed
  const pdf = await generatePDF(invoice);
 
  res.setHeader('Content-Type', 'application/pdf');
  res.send(pdf);
});
 
// User refreshes page = another PDF generated
// User downloads again = another PDF generated
// Cost: $0.0025 Ɨ 3 = $0.0075 for one invoice viewed 3 times

Solution: Cache PDFs

import NodeCache from 'node-cache';
 
const pdfCache = new NodeCache({
  stdTTL: 3600, // 1 hour
  checkperiod: 600
});
 
app.get('/invoice/:id', async (req, res) => {
  const invoiceId = req.params.id;
 
  // Check cache first
  let pdf = pdfCache.get(invoiceId);
 
  if (!pdf) {
    const invoice = await db.invoices.findOne({ id: invoiceId });
 
    // Only generate if not cached
    pdf = await generatePDF(invoice);
 
    // Cache for 1 hour
    pdfCache.set(invoiceId, pdf);
  }
 
  res.setHeader('Content-Type', 'application/pdf');
  res.send(pdf);
});
 
// Same invoice viewed 3 times = only 1 PDF generated
// Cost: $0.0025 Ɨ 1 = $0.0025 (67% savings)

Invalidate Cache When Data Changes

// When invoice is updated, bust cache
app.put('/invoices/:id', async (req, res) => {
  const { id } = req.params;
 
  await db.invoices.update({ id }, req.body);
 
  // Invalidate cached PDF
  pdfCache.del(id);
 
  res.json({ success: true });
});

Use Redis for Distributed Caching

import Redis from 'ioredis';
 
const redis = new Redis(process.env.REDIS_URL);
 
async function getCachedPDF(key) {
  const cached = await redis.getBuffer(key);
  return cached;
}
 
async function cachePDF(key, pdfBuffer, ttl = 3600) {
  await redis.setex(key, ttl, pdfBuffer);
}
 
app.get('/invoice/:id', async (req, res) => {
  const cacheKey = `pdf:invoice:${req.params.id}`;
 
  // Try cache
  let pdf = await getCachedPDF(cacheKey);
 
  if (!pdf) {
    // Generate and cache
    const invoice = await db.invoices.findOne({ id: req.params.id });
    pdf = await generatePDF(invoice);
    await cachePDF(cacheKey, pdf);
  }
 
  res.setHeader('Content-Type', 'application/pdf');
  res.send(pdf);
});

Estimated savings: 40-60% (depending on how often PDFs are re-requested)


Strategy 2: Generate on Demand, Not Proactively

The Problem: Generating PDFs users never download.

// āŒ Generating PDFs proactively
async function createOrder(orderData) {
  const order = await db.orders.insert(orderData);
 
  // Generate PDF immediately, even if user never downloads it
  const pdf = await generatePDF(order);
  await uploadToS3(pdf, `orders/${order.id}.pdf`);
 
  return order;
}
 
// Only 30% of users download the PDF
// 70% of generations are wasted

Solution: Lazy Generation

// āœ… Generate only when requested
async function createOrder(orderData) {
  const order = await db.orders.insert(orderData);
  // Don't generate PDF yet
  return order;
}
 
app.get('/orders/:id/pdf', async (req, res) => {
  const order = await db.orders.findOne({ id: req.params.id });
 
  // Check if PDF already exists
  let pdfUrl = order.pdf_url;
 
  if (!pdfUrl) {
    // Generate on first request
    const pdf = await generatePDF(order);
    pdfUrl = await uploadToS3(pdf, `orders/${order.id}.pdf`);
 
    // Save URL to avoid regenerating
    await db.orders.update({ id: order.id }, { pdf_url: pdfUrl });
  }
 
  res.redirect(pdfUrl);
});

Estimated savings: 50-70% (if only 30-50% of PDFs are actually downloaded)


Strategy 3: Batch Processing

The Problem: Generating PDFs one-by-one is inefficient.

// āŒ Sequential generation
async function sendMonthlyReports(users) {
  for (const user of users) {
    const report = await generateReport(user);
    const pdf = await generatePDF(report); // One at a time
    await sendEmail(user.email, pdf);
  }
}
 
// 1,000 users Ɨ 2 seconds each = 33 minutes total

Solution: Parallel Processing

// āœ… Parallel generation with concurrency control
import pLimit from 'p-limit';
 
async function sendMonthlyReports(users) {
  const limit = pLimit(10); // Max 10 concurrent generations
 
  const tasks = users.map(user => limit(async () => {
    const report = await generateReport(user);
    const pdf = await generatePDF(report);
    await sendEmail(user.email, pdf);
  }));
 
  await Promise.all(tasks);
}
 
// 1,000 users with 10 concurrent = ~3-4 minutes total

Time savings: 85-90% (doesn't reduce cost per PDF, but reduces infrastructure costs)


Strategy 4: Optimize PDF File Size

The Problem: Large PDFs cost more to store and deliver.

// āŒ 5MB PDF stored for 1 year
// Storage: 5MB Ɨ $0.023/GB/month Ɨ 12 months = $0.00138
// Bandwidth: 5MB Ɨ 100 downloads Ɨ $0.09/GB = $0.045
// Total: $0.047 per PDF
 
// āœ… 500KB optimized PDF
// Storage: 0.5MB Ɨ $0.023/GB/month Ɨ 12 months = $0.000138
// Bandwidth: 0.5MB Ɨ 100 downloads Ɨ $0.09/GB = $0.0045
// Total: $0.0047 per PDF
 
// Savings: 90% on storage and bandwidth

Optimize Images

import sharp from 'sharp';
 
async function optimizeImage(imageBuffer) {
  return sharp(imageBuffer)
    .resize(1200, 1200, { fit: 'inside', withoutEnlargement: true })
    .jpeg({ quality: 80, progressive: true })
    .toBuffer();
}
 
// Before embedding in PDF
const optimizedImage = await optimizeImage(originalImage);
const base64 = optimizedImage.toString('base64');
 
const html = `<img src="data:image/jpeg;base64,${base64}">`;

Compress HTML

import { minify } from 'html-minifier';
 
const html = minify(template, {
  collapseWhitespace: true,
  removeComments: true,
  removeRedundantAttributes: true,
  minifyCSS: true,
  minifyJS: true
});
 
// Reduces HTML size by 30-50%

Estimated savings: 20-40% on storage and bandwidth costs


Strategy 5: Smart Storage Lifecycle

The Problem: Storing PDFs forever costs money.

// āŒ Infinite storage
// 10,000 PDFs/month Ɨ 12 months = 120,000 PDFs
// Average 2MB each = 240GB
// Cost: 240GB Ɨ $0.023/GB/month = $5.52/month (and growing)

Solution: Implement Retention Policies

// Set S3 lifecycle rules
const s3LifecycleConfig = {
  Rules: [
    {
      Id: 'DeleteOldInvoices',
      Status: 'Enabled',
      Expiration: {
        Days: 90 // Delete after 90 days
      },
      Filter: {
        Prefix: 'invoices/'
      }
    },
    {
      Id: 'ArchiveOldReports',
      Status: 'Enabled',
      Transitions: [
        {
          Days: 30,
          StorageClass: 'GLACIER' // Move to cheaper storage after 30 days
        }
      ],
      Filter: {
        Prefix: 'reports/'
      }
    }
  ]
};
 
await s3.putBucketLifecycleConfiguration({
  Bucket: 'your-pdf-bucket',
  LifecycleConfiguration: s3LifecycleConfig
}).promise();

Regenerate Instead of Store

// For PDFs that can be easily regenerated
app.get('/reports/:id/pdf', async (req, res) => {
  const report = await db.reports.findOne({ id: req.params.id });
 
  // Don't store, just regenerate on demand
  const pdf = await generatePDF(report);
 
  res.setHeader('Content-Type', 'application/pdf');
  res.setHeader('Content-Disposition', `inline; filename="report-${report.id}.pdf"`);
  res.send(pdf);
 
  // Cost: $0.0025 per generation
  // No storage cost (saving $0.023/GB/month)
});
 
// For infrequently accessed reports, generation cost < storage cost

Estimated savings: 30-50% on storage costs


Strategy 6: Use the Right Templating Engine

The Problem: Complex templating engines cost more CPU time.

Performance Benchmark (1,000 PDFs)

Engine Generation Time Relative Cost
Raw HTML 45s 1x
Handlebars 52s 1.15x
EJS 68s 1.51x
// āŒ Using EJS when you don't need logic
const html = `
<h1><%= invoice.title %></h1>
<p><%= invoice.date %></p>
<p><%= invoice.total %></p>
`;
 
// āœ… Pre-render on server, use raw HTML
const html = `
<h1>${invoice.title}</h1>
<p>${invoice.date}</p>
<p>${invoice.total}</p>
`;
 
// 33% faster generation = 33% cost savings on compute

Estimated savings: 20-30% on generation costs


Strategy 7: CDN for PDF Delivery

The Problem: Serving PDFs from S3 directly is expensive.

// āŒ Direct S3 download
// Bandwidth: $0.09/GB from S3
 
// āœ… Serve via CloudFront CDN
// Bandwidth: $0.085/GB from CloudFront
// + Caching reduces origin requests
 
const pdfUrl = `https://cdn.yourdomain.com/pdfs/${pdfId}.pdf`;
// First request hits S3, subsequent requests served from edge cache

Estimated savings: 5-10% on bandwidth + improved performance


Strategy 8: Monitor and Alert on Costs

The Problem: Cost overruns go unnoticed until the bill arrives.

Track PDF Generation Metrics

import { metrics } from './monitoring';
 
async function generatePDF(data) {
  const startTime = Date.now();
 
  try {
    const pdf = await generatePDFInternal(data);
 
    // Track successful generation
    metrics.counter('pdf.generated', 1, {
      template: data.template_type,
      size: pdf.length
    });
 
    metrics.histogram('pdf.generation.duration', Date.now() - startTime);
 
    return pdf;
 
  } catch (err) {
    // Track failed generation (still costs money!)
    metrics.counter('pdf.generation.failed', 1);
    throw err;
  }
}

Cost Estimation Dashboard

// Calculate estimated monthly cost
async function estimateMonthlyCost() {
  const pdfsThisMonth = await db.pdfs.count({
    created_at: { $gte: startOfMonth() }
  });
 
  const avgPDFSize = await db.pdfs.aggregate([
    { $match: { created_at: { $gte: startOfMonth() } } },
    { $group: { _id: null, avgSize: { $avg: '$file_size' } } }
  ]);
 
  const generationCost = pdfsThisMonth * 0.0025;
  const storageCost = (pdfsThisMonth * avgPDFSize / 1e9) * 0.023;
  const bandwidthCost = (pdfsThisMonth * avgPDFSize / 1e9) * 0.09;
 
  const totalCost = generationCost + storageCost + bandwidthCost;
 
  return {
    pdfs_generated: pdfsThisMonth,
    generation_cost: generationCost,
    storage_cost: storageCost,
    bandwidth_cost: bandwidthCost,
    total_cost: totalCost,
    projected_monthly: (totalCost / new Date().getDate()) * 30
  };
}

Set Budget Alerts

// Check daily
async function checkCostBudget() {
  const costs = await estimateMonthlyCost();
 
  const MONTHLY_BUDGET = 200; // $200/month
 
  if (costs.projected_monthly > MONTHLY_BUDGET) {
    await alerting.notify({
      severity: 'warning',
      message: `PDF costs projected to exceed budget: $${costs.projected_monthly.toFixed(2)} (budget: $${MONTHLY_BUDGET})`,
      details: costs
    });
  }
}

Real-World Case Study: 70% Cost Reduction

Company: E-commerce platform generating 50,000 invoices/month

Before:

  • Generating PDFs proactively for all orders
  • Storing PDFs indefinitely
  • No caching
  • Using EJS templates
  • Cost: $500/month

After implementing:

  1. āœ… Lazy generation (only when downloaded)
  2. āœ… Redis caching with 1-hour TTL
  3. āœ… 90-day retention policy
  4. āœ… Switched to Handlebars
  5. āœ… Image optimization
  6. āœ… CloudFront CDN

Results:

  • Only 30% of orders downloaded = 50% savings immediately
  • 40% cache hit rate = additional 12% savings
  • Smaller file sizes = 15% storage/bandwidth savings
  • Faster templating = 10% compute savings

New cost: $150/month (70% reduction)


Cost Optimization Checklist

  • Cache frequently accessed PDFs (Redis or in-memory)
  • Generate on demand, not proactively
  • Batch process bulk generations
  • Optimize image sizes before embedding
  • Implement storage lifecycle policies
  • Use simplest templating engine that works
  • Serve PDFs via CDN
  • Monitor costs and set budget alerts
  • Delete/archive old PDFs
  • Pre-render templates when possible

When NOT to Optimize

Premature optimization is the root of all evil.

Don't optimize if:

  • You're generating < 1,000 PDFs/month (cost is negligible)
  • Your current cost is < $50/month (time > money)
  • Complexity of optimization exceeds savings

Do optimize if:

  • You're generating > 10,000 PDFs/month
  • Your bill is > $200/month
  • You're growing rapidly (costs will compound)

The reportgen.io Advantage

Some optimizations are built into reportgen.io:

āœ… No per-request overhead - Only pay $0.0025 per PDF āœ… Unlimited file size - No premium for large PDFs āœ… Global CDN included - No extra bandwidth costs āœ… No storage fees - 1-year retention included āœ… Unlimited concurrency - Batch without limits


Final Thoughts

PDF generation doesn't have to be expensive.

With smart caching, lazy generation, and lifecycle policies, you can cut costs by 50-70% without sacrificing quality or performance.

Start with the high-impact strategies:

  1. Cache aggressively
  2. Generate on demand
  3. Delete/archive old PDFs

The rest is optimization on the margins.

Ready for transparent, predictable pricing? Try reportgen.io - $0.0025 per PDF, no hidden fees.