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

Use Node.js as the scheduler and PhantomJS as a separate rendering process. PhantomJS is not a Node.js module: your Node program should launch one PhantomJS child process per URL (or per bounded batch), pass the URL and output path as arguments, and collect each process’s exit status. The PhantomJS script creates a webpage, sets its viewport, opens the page, renders only after a successful load, and exits explicitly.

This approach still works for legacy capture jobs, but PhantomJS development is suspended and its upstream repository is archived and read-only. The instructions below use the PhantomJS 2.1/2.1.1 documentation context, so validate the binary on your operating system before committing it to a production pipeline.

What the batch architecture looks like

There are two programs with a clear process boundary:

  • Node.js controller: reads URLs, creates unique filenames, limits concurrent work, launches PhantomJS, applies a controller timeout, and records results.
  • PhantomJS worker: receives one URL and one destination, configures the page, calls page.open(), renders on success, reports errors, and exits.

PhantomJS is invoked as a command-line executable with a script and arguments. Treating it as a normal require()-able Node package is the wrong integration model. A separate process also keeps a failed page from crashing the controller and lets you log an exit code for every input.

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

Prerequisites and directory layout

  • Node.js installed and available as node.
  • A PhantomJS 2.1.x executable available as phantomjs (or an absolute path).
  • A writable output directory.
  • Network access to each target site, including any authentication or proxy configuration required by that site.

Create a directory such as:

batch-shots/
  batch.js
  capture.js
  urls.txt
  shots/

Put one URL per line in urls.txt. Blank lines are ignored by the controller.

Write the PhantomJS capture script

Save this as capture.js. It reads its arguments through PhantomJS’s system module. The viewport controls the browser’s layout dimensions. Set clipRect only when you want a crop rather than the full rendered viewport.

var system = require('system');
var page = require('webpage').create();

var url = system.args[1];
var output = system.args[2];

if (!url || !output) {
  console.error('Usage: phantomjs capture.js URL OUTPUT');
  phantom.exit(2);
}

page.viewportSize = { width: 1280, height: 800 };
// Optional crop:
// page.clipRect = { top: 0, left: 0, width: 1280, height: 800 };

page.open(url, function (status) {
  if (status === 'success') {
    page.render(output);
    console.log(JSON.stringify({ url: url, output: output, status: status }));
    phantom.exit(0);
  }

  console.error('Failed to load: ' + url + ' (status: ' + status + ')');
  phantom.exit(1);
});

The success check is essential. Do not render an error page and label it a successful capture. The explicit phantom.exit() prevents a completed worker from keeping the child process alive.

Output formats and dimensions

PhantomJS’s screen-capture API supports PNG, JPEG, GIF, and PDF output. The output filename generally determines the format, but behavior can vary with the installed build; verify the version when a particular format matters. For example, use shots/example.png or shots/example.pdf. viewportSize affects responsive layout, while clipRect limits the saved region.

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

Build a bounded Node.js controller

Save the following as batch.js. It launches no more than four workers at once; that number is an example, not a PhantomJS requirement. Tune it for your machine, memory, network and target sites. Each URL receives a deterministic hash-based filename, so query strings and two similar URLs cannot accidentally overwrite one another.

const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');
const { spawn } = require('node:child_process');

const phantom = process.env.PHANTOMJS || 'phantomjs';
const worker = path.join(__dirname, 'capture.js');
const input = path.join(__dirname, 'urls.txt');
const outputDir = path.join(__dirname, 'shots');
const concurrency = 4;       // Example; tune empirically.
const timeoutMs = 90_000;    // Controller safeguard, not a PhantomJS default.

fs.mkdirSync(outputDir, { recursive: true });
const urls = fs.readFileSync(input, 'utf8')
  .split(/r?n/)
  .map(s => s.trim())
  .filter(Boolean);

function outputFor(url) {
  const digest = crypto.createHash('sha256').update(url).digest('hex').slice(0, 16);
  return path.join(outputDir, `${digest}.png`);
}

function runOne(url) {
  return new Promise(resolve => {
    const output = outputFor(url);
    const child = spawn(phantom, [worker, url, output], {
      stdio: ['ignore', 'pipe', 'pipe']
    });
    let stdout = '';
    let stderr = '';
    let timedOut = false;
    const timer = setTimeout(() => {
      timedOut = true;
      child.kill('SIGTERM');
    }, timeoutMs);

    child.stdout.on('data', chunk => { stdout += chunk; });
    child.stderr.on('data', chunk => { stderr += chunk; });
    child.on('error', err => {
      clearTimeout(timer);
      resolve({ url, output, ok: false, code: null, error: err.message });
    });
    child.on('close', code => {
      clearTimeout(timer);
      const ok = !timedOut && code === 0 && fs.existsSync(output);
      resolve({
        url, output, ok, code,
        timedOut,
        stdout: stdout.trim(),
        stderr: stderr.trim()
      });
    });
  });
}

async function main() {
  const results = [];
  let next = 0;
  async function workerLoop() {
    while (true) {
      const index = next++;
      if (index >= urls.length) return;
      results[index] = await runOne(urls[index]);
      console.log(JSON.stringify(results[index]));
    }
  }
  await Promise.all(
    Array.from({ length: Math.min(concurrency, urls.length) }, workerLoop)
  );
  const failures = results.filter(result => !result.ok);
  console.error(`${results.length - failures.length} succeeded, ${failures.length} failed`);
  process.exitCode = failures.length ? 1 : 0;
}

main().catch(error => {
  console.error(error);
  process.exitCode = 1;
});

Run it with:

node batch.js

To use a non-default executable, set PHANTOMJS:

PHANTOMJS=/opt/phantomjs/bin/phantomjs node batch.js

Why the controller checks more than an exit code

  • URL: identifies the input that failed.
  • Output path: tells you which artifact should exist.
  • Load status: comes from page.open().
  • Exit code: distinguishes a successful worker from a failed one.
  • stderr and timeout: preserve diagnostic text and identify a stuck child.

The example also checks that the output file exists. A stale file from a previous run must not turn a new failed load into a false success; for stricter pipelines, remove an existing destination before launching the worker and record its byte size after completion.

Common capture requirements

Full-page versus viewport capture

The script above captures the configured viewport. A long document may require a full-page strategy, but legacy PhantomJS behavior can depend on page layout and version. Test pages with lazy content and fixed-position elements rather than assuming a viewport render equals a complete document.

Only one region

Uncomment and adjust page.clipRect. Its top, left, width and height values define the saved rectangle.

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

Authenticated or customized pages

PhantomJS can be configured further inside the worker for the target environment, but credentials should not be placed directly in a URL or committed to source. If a site requires a modern browser feature that PhantomJS does not implement, a successful process is not guaranteed to produce a faithful page.

Troubleshooting

“phantomjs: command not found” or spawn ENOENT

Install PhantomJS for the target operating system or set PHANTOMJS to its absolute path. Check the executable permission and run phantomjs --version independently.

Every job reports “Failed to load”

Check DNS, TLS compatibility, proxy rules and the exact URL. Open the same address from the machine running the batch. Sites that require browser capabilities unavailable to this legacy engine may never reach success.

The process never finishes

Keep the controller timeout. Inspect stderr, then reduce concurrency and test the URL alone. A timeout is a failed job; do not reuse its old output.

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

Files overwrite one another

Do not derive names from only the hostname. The hash-based name includes the complete URL. If you change naming, preserve uniqueness for paths, query strings and fragments.

The image has the wrong responsive layout

Change page.viewportSize before page.open(). The viewport is part of the page’s rendering inputs, so a mobile width can produce a different layout than a desktop width.

The output extension is not honored

Try a format documented by your installed PhantomJS build and inspect the resulting file. Format support and edge behavior should be verified against the binary you deploy.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Performance, reliability and maintenance

Launching one PhantomJS process per URL is simple and isolates failures, but process startup and browser memory are real costs. More parallel workers do not automatically mean faster completion: they can exhaust memory, saturate bandwidth or trigger target-site throttling. Start with a small limit, measure your own workload, and increase it only while error rates and resource use remain acceptable. No general safe concurrency or throughput figure is established for PhantomJS.

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

For repeatable jobs, persist the JSON result for every URL, retry only explicitly classified transient failures, and keep the original URL beside the artifact. Make output directories run-specific when reproducibility matters. Validate the PhantomJS binary on every operating-system image you deploy. PhantomJS’s upstream project is archived and read-only, and development is suspended; its 2.1/2.1.1 documentation is a legacy compatibility reference, not a promise of current web-platform support.

When a hosted renderer is a better operational fit

A local PhantomJS batch gives you process-level control and local files, but you own installation, browser compatibility, retries, concurrency and monitoring. PhantomJSCloud documentation describes screenshot rendering and batch requests through a Node.js client, which can remove some local process management; current service pricing, limits, availability and performance are not established here, so verify those details directly before selecting it.

Or skip the browser setup

ScreenshotNeo is the first alternative to try when you want an HTTP screenshot API instead of maintaining PhantomJS: it removes cookie banners, newsletter popups and chat widgets before capture, bills only clean shots, and does not bill bot checks/CAPTCHAs, blank pages, timeouts, failed loads or cache hits. Every response identifies the page verdict and billing state with X-Page-Verdict and X-Billed headers. It also provides an MCP server for Claude, Cursor and other MCP clients, with take_screenshot, get_page_info and capture_pdf tools.

One GET request returns an image or PDF. See the ScreenshotNeo API documentation for all options, including bulk capture of up to 100 URLs per call, signed webhooks for asynchronous jobs, custom CSS and JavaScript, device and viewport controls, PDF page ranges, headers, cookies, geolocation, caching and element capture.

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.
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}`);

The Free plan includes 1,000 screenshots a month with no card. Paid plans start at $5 for 3,000 screenshots; every feature is available on every plan. Create a free ScreenshotNeo account.

FAQ

Is PhantomJS a Node.js package?

No. Run its executable as a child process and communicate through arguments, output and exit status.

Can I use a single PhantomJS process for every URL?

You can design a persistent worker, but the one-process-per-URL pattern is easier to isolate and diagnose. Any persistent design needs explicit job boundaries and recovery handling.

Does PhantomJS execute modern JavaScript?

Its legacy engine may not support APIs or syntax required by current sites. Test representative targets and treat compatibility as a deployment risk.

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

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.