Heroku免费层运行多实例Puppeteer生成PDF超时问题排查求助
Hey there, let's dig into why your app is timing out on Heroku when generating more than 2 PDFs—and how to fix it. Your hunch about Heroku's 512MB free tier memory limit is spot-on, but there are specific issues in your code that are exacerbating this problem.
Root Cause Analysis
The biggest issue in your current code is that you're launching a new Puppeteer browser instance for every PDF and never closing them. Each Chromium browser instance is memory-heavy—even in headless mode, it can take 100MB+ of RAM per instance. By the time you get to the 3rd PDF, you're running 3 separate browsers, which quickly eats up Heroku's limited memory, leading to timeouts or crashes.
Additionally, you're not cleaning up the PDF files you generate (Heroku's filesystem is ephemeral, but leaving files around can still contribute to resource bloat), and you might be hitting limits on shared memory with default Puppeteer settings.
Troubleshooting Steps
- Check Heroku Memory Usage: Use
heroku logs --tailto watch for out-of-memory (OOM) errors, or go to your Heroku app's Metrics tab to monitor RAM usage while generating PDFs. You'll likely see it spike to near 512MB when generating the 3rd PDF. - Verify Browser Instance Leaks: Look for logs showing how many browser processes are running—if you're not closing them, they'll linger until Heroku kills your dyno.
Fixes & Optimizations
Here are actionable changes to get your app working with more than 2 PDFs:
1. Reuse a Single Browser Instance
Instead of launching a new browser for each PDF, create one browser before the loop and reuse it for all pages. This cuts down on memory overhead drastically.
2. Always Close Pages & Browser
Make sure to close each page after generating the PDF, and close the browser once all PDFs are done. This frees up memory immediately.
3. Add Puppeteer Flags for Heroku Compatibility
Heroku's environment has limitations with shared memory, so add these args to your Puppeteer launch config:
--disable-dev-shm-usage: Bypasses the small/dev/shmpartition on Heroku, which can cause crashes with multiple pages.--no-sandbox/--disable-setuid-sandbox: You already have these, but they're essential for running Chromium on Heroku.
4. Clean Up Generated PDF Files
Since Heroku's filesystem is temporary, delete each PDF after adding it to the email attachment (or use in-memory generation to avoid writing files at all).
5. Limit Concurrency (If Needed)
If you're generating a lot of PDFs at once, limit how many pages you process concurrently to avoid overwhelming memory.
Modified Code Example
Here's how your code should look with these fixes:
// Launch a single browser instance outside the loop const browser = await puppeteer.launch({ headless: true, args: [ "--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", // Critical for Heroku "--disable-gpu", // Optional, saves more memory "--single-process" // Optional, reduces memory footprint (test first) ], }); try { for (let x = 1; x <= numTickets; x++) { console.log(x, " / ", numTickets); let page; try { page = await browser.newPage(); // Compile HTML const content = await compile(template, { billing, id: `${id}-${x}`, campaign, }); const pdfFilename = `${billing.first_name}-housedoubleup-${id}-${x}.pdf`; const pdfPath = `./${pdfFilename}`; options.attachments.push({ filename: pdfFilename, path: pdfPath, contentType: "application/pdf", }); await page.setContent(content); await page.emulateMediaType("screen"); await page.pdf({ path: pdfPath, format: "a5", printBackground: true, landscape: true, }); console.log(`PDF ${x} done`); // Optional: Delete the file after attaching (free up disk space) const fs = require('fs').promises; await fs.unlink(pdfPath); } catch (e) { console.log(`Error generating PDF ${x} -> `, e); } finally { // Always close the page to free memory if (page) await page.close(); } } // Send email after all PDFs are processed await sendEmail(options); } catch (emailErr) { console.log("Error sending email -> ", emailErr); } finally { // Always close the browser when done await browser.close(); }
Bonus: In-Memory PDF Generation (Avoid Files Altogether)
Instead of writing PDFs to disk, you can generate them as buffers and attach them directly to the email. This saves disk space and avoids cleanup:
// Replace the page.pdf call with: const pdfBuffer = await page.pdf({ format: "a5", printBackground: true, landscape: true, }); // Then add to attachments as a buffer: options.attachments.push({ filename: pdfFilename, content: pdfBuffer, contentType: "application/pdf", });
This eliminates the need to write and delete files entirely, which is cleaner and more efficient on Heroku.
Final Notes
If you still hit memory limits with these changes, consider upgrading to Heroku's Hobby tier (which has 1GB of RAM) or optimizing your HTML template to reduce the amount of resources Chromium has to load (e.g., remove unnecessary images, CSS, or scripts).
内容的提问来源于stack exchange,提问作者J.Hansen

