Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Prevent hanging Chromium processes on Heroku by making browser shutdown part of every job’s guaranteed cleanup and by treating dyno shutdown as normal. Launch Chromium inside a bounded job, close it in a finally block, stop accepting new work when shutdown starts, and give active jobs a finite window to finish. Keep Puppeteer’s signal handling enabled unless you have a documented reason to change it. Heroku cycles dynos at least daily, so no browser process should be treated as permanent.

What actually causes a Chromium process to linger

A Chromium process can remain after a request when an exception, timeout, rejected promise, or early return bypasses cleanup. The same problem appears when a worker starts shutting down while a browser is still open, or when code launches a new browser for every request without controlling how many jobs can run at once.

There are two separate concerns:

  • Job cleanup: every successful, failed, and timed-out job must reach browser.close().
  • Process shutdown: the application must stop taking new work, finish or cancel active jobs, close any remaining browsers, and exit before Heroku’s shutdown deadline.

Launch configuration solves neither concern by itself. Puppeteer documents handleSIGHUP, handleSIGINT, and handleSIGTERM launch options as enabled by default. Those handlers help connect operating-system signals to browser shutdown, but your application still needs a job-level cleanup path.

Use a job-scoped browser with guaranteed cleanup

The safest baseline for short-lived work is to keep the browser’s lifetime inside one function. Declare the variable before the try block so a failed launch does not create a second error while cleaning up.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
#1 Best Overall
CanaKit Raspberry Pi 5 Starter Kit PRO - Turbine Black (128GB Edition) (8GB RAM)
  • Includes Raspberry Pi 5 with 2.4Ghz 64-bit quad-core CPU (8GB RAM)
  • Includes 128GB Micro SD Card pre-loaded with 64-bit Raspberry Pi OS, USB MicroSD Card Reader
  • CanaKit Turbine Black Case for the Raspberry Pi 5
  • CanaKit Low Noise Bearing System Fan
  • Mega Heat Sink - Black Anodized
const puppeteer = require('puppeteer');

async function capturePage(url, signal) {
  let browser;

  try {
    browser = await puppeteer.launch({
      // Add the arguments required by your Heroku runtime and security policy.
      // Puppeteer signal handling remains enabled by default.
      handleSIGHUP: true,
      handleSIGINT: true,
      handleSIGTERM: true,
      signal
    });

    const page = await browser.newPage();
    page.setDefaultNavigationTimeout(30000);
    page.setDefaultTimeout(30000);

    await page.goto(url, {
      waitUntil: 'networkidle2',
      timeout: 30000,
      signal
    });

    return await page.screenshot({ type: 'png' });
  } finally {
    if (browser) {
      await browser.close();
    }
  }
}

capturePage('https://example.com').then(() => {
  console.log('job complete');
}).catch((error) => {
  console.error('job failed', error);
});

The finally block runs after a normal return and after most exceptions. Keep waits bounded: navigation, selectors, network-idle waits, downloads, and application-level queues can otherwise keep the function alive until Heroku terminates the process. Use an AbortSignal where the Puppeteer API and your installed version support it; verify option names against the version deployed by your application. Puppeteer’s API reference lists version 25.12.0, while your lockfile may select another release.

Do not hide cleanup failures

If browser.close() itself rejects, log that error with the job identifier. A cleanup failure is operationally different from a page-load failure and should be visible in your logs. If you need to preserve the original job error, wrap cleanup separately:

let jobError;
try {
  // work
} catch (error) {
  jobError = error;
} finally {
  try {
    if (browser) await browser.close();
  } catch (closeError) {
    console.error('chromium close failed', closeError);
  }
}
if (jobError) throw jobError;

Handle Heroku shutdown as a normal event

Heroku says dynos restart at least once per day at randomized times. A worker therefore needs a shutdown path even when every individual request normally succeeds. Heroku’s shutdown guidance also illustrates an R12 exit timeout after 30 seconds. Your application should stop intake immediately and leave enough time for active work and browser closure.

Track active jobs and stop intake

The following pattern is suitable for a long-running worker or HTTP process. The queue or server-specific “stop intake” operation is represented by a function you must connect to your framework.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Rank #2
CanaKit Raspberry Pi 4 4GB Starter PRO Kit - 4GB RAM
  • Includes Raspberry Pi 4 4GB Model B with 1.5GHz 64-bit quad-core CPU (4GB RAM)
  • Includes Pre-Loaded 32GB EVO+ Micro SD Card (Class 10), USB MicroSD Card Reader
  • CanaKit Premium High-Gloss Raspberry Pi 4 Case with Integrated Fan Mount, CanaKit Low Noise Bearing System Fan
  • CanaKit 3.5A USB-C Raspberry Pi 4 Power Supply (US Plug) with Noise Filter, Set of Heat Sinks, Display Cable - 6 foot (Supports up to 4K60p)
  • CanaKit USB-C PiSwitch (On/Off Power Switch for Raspberry Pi 4)
const activeJobs = new Set();
const browsers = new Set();
let shuttingDown = false;

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function runJob(url) {
  if (shuttingDown) {
    throw new Error('worker is shutting down');
  }

  let browser;
  const job = (async () => {
    try {
      browser = await puppeteer.launch({
        handleSIGHUP: true,
        handleSIGINT: true,
        handleSIGTERM: true
      });
      browsers.add(browser);

      const page = await browser.newPage();
      page.setDefaultNavigationTimeout(30000);
      await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
      return page.url();
    } finally {
      if (browser) {
        browsers.delete(browser);
        await browser.close();
      }
    }
  })();

  activeJobs.add(job);
  try {
    return await job;
  } finally {
    activeJobs.delete(job);
  }
}

async function shutdown(signal) {
  if (shuttingDown) return;
  shuttingDown = true;
  console.log(`received ${signal}; stopping new jobs`);

  // Stop your HTTP server, queue consumer, or scheduler here.
  // server.close(() => ...), for example, prevents new requests.

  const jobs = [...activeJobs];
  await Promise.race([
    Promise.allSettled(jobs),
    sleep(25000)
  ]);

  // Close anything that remained after the bounded wait.
  await Promise.allSettled(
    [...browsers].map(browser => browser.close())
  );

  process.exit(0);
}

process.on('SIGTERM', () => { void shutdown('SIGTERM'); });
process.on('SIGINT', () => { void shutdown('SIGINT'); });

The 25-second limit in this example is an application choice that leaves margin under the 30-second timeout illustrated by Heroku; adjust it to your platform behavior and workload. Do not call process.exit(0) immediately when a signal arrives. Doing so can terminate JavaScript before finally blocks and browser close operations run.

HTTP servers need an intake barrier

Set a shutdown flag before closing the server so handlers that were already accepted can finish while new requests receive a fast error. For a queue, pause consumption and acknowledge or requeue messages according to that queue’s delivery guarantees. A graceful shutdown is not complete until the producer has stopped creating work and the consumer has stopped starting browsers.

Heroku deployment details that are easy to confuse with cleanup

Install the browser dependencies

Puppeteer’s Heroku troubleshooting guidance says Heroku does not provide every system dependency Chromium needs and recommends adding the Puppeteer Heroku buildpack. Confirm that the buildpack version, browser revision, Node.js runtime, and installed Puppeteer version are compatible before deploying. A missing shared library or incompatible executable prevents launch; it does not explain a browser that remains open after a completed job.

Use --no-sandbox only as deployment-specific configuration

The same Heroku guidance advises --no-sandbox in its deployment section. Treat this as a runtime and security decision for the actual dyno environment, not as a process-leak fix. If you use it, document why it is required, review the security implications, and keep the argument in the launch configuration rather than scattering it through job code.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Rank #3
ELECROW CrowPi Case Kit for Raspberry Pi 5, 9-Inch Display
  • Not including the Raspberry Pi 5 (8GB), the Crowpi advanced version comes with the Raspberry Pi 5
  • ELECROW Black Case for the Raspberry Pi 5, CrowPi is equipped with a 9-inch HD touchscreen along with a camera; All the regular components used in DIY electronics are packed into the CrowPi development board, such as LCD, LED matrix, buzzer, light sensor, PIR sensor, ultrasonic sensor, IR sensor, etc
  • Raspberry Pi Sensors: The Crowpi raspberry pi 5 programming kit is jam-packed with lots of buttons such as 19 different sensors in a tidy easy to use package; You don't have to wait and wire things
  • Build Quality: Solid ABS shell and well made components in one place make it strong and convenient to travel
  • Programming Lessons: This raspberry pi 5 learning kit ships with step by step instructions and provides 21 lessons to take you through identifying components reading code and running it in the terminal
const browser = await puppeteer.launch({
  args: ['--no-sandbox'],
  handleSIGHUP: true,
  handleSIGINT: true,
  handleSIGTERM: true
});

Do not apply Docker PID 1 advice automatically

Puppeteer’s troubleshooting page discusses --init and dumb-init for Docker, where PID 1 must reap zombie processes. That advice is relevant only when your Heroku deployment actually runs the application in a container with the corresponding PID 1 behavior. It is not a universal remedy for a standard Heroku dyno and cannot replace browser.close().

Design for bounded work and controlled concurrency

  • Set explicit navigation and operation timeouts; do not rely on a network-idle event that may never occur because of analytics, websockets, or long polling.
  • Keep a browser reference in a scope that the shutdown handler can reach, or ensure every job owns and closes its browser in finally.
  • No official documentation establishes a safe universal concurrency or memory number; measure your application on its chosen dyno size.
  • Decide deliberately between one browser per job and a reused browser. Reuse can reduce launch overhead, but a reused browser must still be closed during shutdown and replaced when it becomes unhealthy. Per-job browsers simplify isolation but increase launch work.
  • Keep jobs short enough to finish inside the platform’s shutdown window, or make them resumable so a dyno restart does not lose the work.
  • Record job start, URL or job ID, browser launch result, page completion, close result, and shutdown signal. These timestamps let you distinguish a slow page from a skipped cleanup path.

Diagnose hanging processes with Heroku logs

Use the Heroku CLI to correlate platform events with your application’s lifecycle logs. Heroku’s CLI reference documents heroku logs, heroku ps:restart web, and process-scaling commands.

heroku logs --tail
heroku ps
heroku ps:restart web

A restart can restore service, but it does not correct a missing cleanup path. When you see an exit timeout or an unexpected dyno stop, inspect the preceding logs for these questions:

  1. Did the process log that it stopped accepting new work?
  2. Which jobs were still active when shutdown began?
  3. Did each job reach its finally block?
  4. Did browser.close() resolve or reject?
  5. Were Puppeteer signal handlers disabled or replaced by application code?
  6. Was a new browser launched after shutdown had started?

Common symptoms and fixes

Symptom Likely cause Fix
Chromium remains after a navigation error Close logic exists only on the success path. Move closure into finally and add a bounded navigation timeout.
Dyno reports an exit timeout during deploy or restart Active jobs, queue intake, or browser shutdown is still pending. Stop intake on the signal, wait for active jobs with a deadline, then close remaining browsers.
Launch fails with missing library or executable errors Heroku lacks a Chromium dependency or the buildpack/runtime does not match Puppeteer. Install and verify the Puppeteer Heroku buildpack and align Node, Puppeteer, and browser versions.
Memory pressure grows as traffic rises Each request launches a browser without a concurrency bound, or a reused browser accumulates state. Use a queue or semaphore, close pages and browsers deterministically, and measure resource use on the selected dyno.
Adding --init changes nothing The app is not running in the Docker PID 1 scenario described by Puppeteer. Remove unrelated changes and repair the application’s browser and shutdown lifecycle.
Restart fixes the symptom temporarily The platform killed accumulated processes without fixing the code path. Use logs to locate the job that skipped cleanup and add a regression test for failure and signal paths.

Test the lifecycle before production

  1. Run a successful capture and verify that the close log follows it.
  2. Force navigation to a URL that times out and verify that cleanup still runs.
  3. Throw an error after opening a page and verify the browser closes.
  4. Send a termination signal while a capture is waiting and verify that intake stops and the process exits within the configured bound.
  5. Deploy with the exact Node.js, Puppeteer, buildpack, and browser revisions used in production; do not infer compatibility from a local desktop installation.

Or skip the browser setup

If your goal is dependable website images or PDFs rather than operating Chromium on a dyno, ScreenshotNeo provides a website screenshot API and MCP server. A GET request returns PNG, JPEG, WebP, or PDF, while the service accepts the cookie or consent banner like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture. Those cleanup steps can be enabled or disabled.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Only clean shots are billed. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and each response identifies the result with X-Page-Verdict and X-Billed headers. The MCP server exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.

Rank #4
CanaKit Raspberry Pi 5 Desktop PC with SSD (Fully Assembled) (256 GB SSD)
  • Fully assembled for plug-and-play operation
  • Includes Raspberry Pi 5 with 8GB RAM
  • 256 GB PCIe Pi NVMe SSD (Pre-loaded with Pi 64-Bit OS)
  • M.2 HAT+
  • CanaKit Turbine Black Case for the Pi 5

See the ScreenshotNeo API documentation for authentication and options. The one-call examples below use the supplied endpoint:

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 with lazy images loaded, CSS-selector element capture, dark mode, 12 device presets plus custom viewports, retina scale, PDF paper sizes and page ranges, custom CSS and JavaScript, click-before-capture, selector hiding, selector or network-idle waits, ad and tracker blocking, custom headers, cookies, user agents and authorization, timezone and geolocation, transparent backgrounds, resizing, configurable-TTL caching, signed image links, asynchronous jobs with signed webhooks, bulk capture for 100 URLs per call, a usage API, an OpenAPI specification, and compatibility with parameter names used by other screenshot APIs.

The Free plan includes 1,000 shots per month with no card. Paid plans start at $5 for 3,000 shots; yearly billing provides two months free, and every feature is available on every plan. Sign up free for ScreenshotNeo.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

FAQ

Should a dyno restart be considered a cleanup strategy?

No. A restart is a platform lifecycle event that can recover a stuck process, but it can also interrupt work. Cleanup belongs in the application so jobs release resources before the dyno exits.

Best Value
Sale
RasTech Raspberry Pi 5 8GB Kit with Active Cooler and Pi5 Case
  • 【What you Get】You will get 1*Pi 5 8GB Single Board,1*RasTech Case,1*Active Cooler,1*Screwdriver,1*Installation instructions,12-month free warranty, lifetime service, 24-hour prompt and friendly response.
  • 【More Connectors】There are two USB 3.0 ports(5Gbps simultaneously) and two USB 2.0 ports, which triple total bandwidth ,support any combination of up to two cameras or displays. Peak SD card performance is doubled through support for the SDR104 high-speed mode. It provides a smooth desktop experience for you. Offer Gigabit Ethernet and a PCIe interface, along with dual-band Wi-Fi and Bluetooth 5.0/BLE wireless capability. The RasTech Pi 5 Kit use the new 27W 5.1V 5A USB-C power connector.
  • 【 Support Dual 4Kp60 Display 】Each of the two microHDMI sockets can control a 4K display at 60 Hertz, now support HDR, offering super HD video for media streaming projects. RPi 5 is the first RPi model that comes with a PCI Express port (PCIe 2.0 x1 with 500 MB/s) to attach SSDs (requires separate M.2 HAT).
  • 【 Excellent Chips And Applications】Pi 5 is a full-size Pi computer using silicon built in-house at Pi. The RP1 “southbridge” provides the bulk of the I/O capabilities for Pi 5. Pi 5 is more friendly and convenient in the development of Internet of Things, Web development, machine identification, automatic control and other electronic equipment applications and network.
  • 【 Faster CPU, Better GPU 】 Pi 5 features a Broadcom BCM2712 64-bit quad-core Arm Cortex-A76 processor running at 2.4GHz, it delivers a 2–3× increase in CPU performance relative to RaspberryPi 4. The 800MHz VideoCore VII GPU is compatible to OpenGL ES 3.1 and Vulkan 1.2, substantial uplift in graphics performance. Pi 5 Offers lightning-fast CPU speed, a PCI Express interface, a Real Time Clock (RTC) and a power button and runs significantly cooler than Pi 4.

What should I verify after upgrading Puppeteer?

Check the installed API and browser revision against the launch options, buildpack, Node.js runtime, and Heroku deployment method. Signal and abort options are version-sensitive, so validate them against the release in your lockfile.

Why can a page that looks finished still block shutdown?

Network-idle waits, timers, open connections, downloads, and queue callbacks can remain pending after the visible page appears complete. Give each operation a timeout and make shutdown stop new work before waiting on the active set.

Frequently Asked Questions

Can I rely on Heroku’s daily dyno cycling to clear Chromium?

No. Cycling may terminate a process, but it can interrupt jobs and does not repair a code path that skips browser cleanup.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What should I check when Puppeteer options change after an upgrade?

Verify the options against the Puppeteer version in your lockfile and recheck its browser revision, Heroku buildpack, and Node.js runtime together.

Why can a visually complete page still prevent shutdown?

Network-idle waits, timers, open connections, downloads, or queue callbacks may still be pending; bound each operation and stop intake before waiting for active jobs.

Quick Recap

Bestseller No. 1
CanaKit Raspberry Pi 5 Starter Kit PRO - Turbine Black (128GB Edition) (8GB RAM)
CanaKit Raspberry Pi 5 Starter Kit PRO - Turbine Black (128GB Edition) (8GB RAM)
Includes Raspberry Pi 5 with 2.4Ghz 64-bit quad-core CPU (8GB RAM); CanaKit Turbine Black Case for the Raspberry Pi 5
$259.95
Bestseller No. 2
CanaKit Raspberry Pi 4 4GB Starter PRO Kit - 4GB RAM
CanaKit Raspberry Pi 4 4GB Starter PRO Kit - 4GB RAM
Includes Raspberry Pi 4 4GB Model B with 1.5GHz 64-bit quad-core CPU (4GB RAM); Includes Pre-Loaded 32GB EVO+ Micro SD Card (Class 10), USB MicroSD Card Reader
$159.99
Bestseller No. 4
CanaKit Raspberry Pi 5 Desktop PC with SSD (Fully Assembled) (256 GB SSD)
CanaKit Raspberry Pi 5 Desktop PC with SSD (Fully Assembled) (256 GB SSD)
Fully assembled for plug-and-play operation; Includes Raspberry Pi 5 with 8GB RAM; 256 GB PCIe Pi NVMe SSD (Pre-loaded with Pi 64-Bit OS)
$339.97

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.