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

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Use Node.js as the batch controller and PhantomJS as a separate renderer. PhantomJS is a command-line program, not a Node.js module. Your Node process should read URLs, start a bounded number of PhantomJS child processes, pass each URL and output path as arguments, and record each process result. This design still works for legacy capture jobs, but PhantomJS development is suspended and its upstream repository is archived, so validate PhantomJS 2.1.1 on your target operating system before depending on it.

What the architecture looks like

Each screenshot is handled by two programs:

  • Node.js controller: loads the URL list, creates collision-safe filenames, limits concurrency, starts PhantomJS, applies a timeout, and reports status.
  • PhantomJS script: creates a webpage, sets the viewport, opens one URL, checks the load status, renders an image, then exits with code 0 or 1.

The PhantomJS FAQ describes this as a “loose binding”: launch a PhantomJS process from Node.js and interact with it. Do not try to require('phantomjs') as though it were a normal browser automation module.

Prerequisites and version caveats

  • Node.js with permission to create child processes and write the destination directory.
  • A PhantomJS executable available on PATH, or an absolute path to the executable.
  • A PhantomJS capture script saved as a separate file.
  • A URL list containing absolute http:// or https:// URLs.

PhantomJS 2.1.1 is the version context used by the project’s command-line documentation; the repository README identifies 2.1 as the latest stable release. The project is archived and read-only, and development is suspended. Expect compatibility issues with modern TLS, JavaScript, sites that require current browser features, and bot protection. Test a representative set of pages on the operating system where the batch will run.

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

Build the PhantomJS renderer

Create capture.js:

var system = require('system');
var webpage = require('webpage');

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

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

var page = webpage.create();
page.viewportSize = { width: 1280, height: 800 };

// Optional crop: uncomment and adjust when you need a fixed region.
// 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);
});

Arguments are read through PhantomJS’s system module. viewportSize controls the browser viewport. Set clipRect when you need only a rectangular region rather than the whole rendered viewport. The extension of the output filename determines the intended format; PhantomJS documentation lists PNG, JPEG, GIF, and PDF support. Verify behavior with the installed build when a particular format is important.

Write a bounded Node.js batch controller

Save the following as batch-screenshots.js. It uses only Node’s built-in modules, limits simultaneous PhantomJS processes, applies a per-job timeout, and keeps a result for every input URL.

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 inputFile = process.argv[2] || 'urls.txt';
const outputDir = process.argv[3] || 'shots';
const concurrency = Number(process.env.CONCURRENCY || 3); // tune for your machine
const timeoutMs = Number(process.env.TIMEOUT_MS || 60000);

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

function outputName(url, index) {
  const digest = crypto.createHash('sha256').update(url).digest('hex').slice(0, 12);
  return path.join(outputDir, `${String(index + 1).padStart(4, '0')}-${digest}.png`);
}

function runOne(url, index) {
  return new Promise(resolve => {
    const output = outputName(url, index);
    const child = spawn(phantom, ['capture.js', url, output], { windowsHide: true });
    let stderr = '';
    let finished = false;
    const timer = setTimeout(() => {
      if (!finished) {
        finished = true;
        child.kill('SIGTERM');
        resolve({ url, output, ok: false, error: 'timeout' });
      }
    }, timeoutMs);

    child.stderr.on('data', data => { stderr += data.toString(); });
    child.on('error', error => {
      if (!finished) {
        finished = true; clearTimeout(timer);
        resolve({ url, output, ok: false, error: error.message });
      }
    });
    child.on('close', code => {
      if (finished) return;
      finished = true; clearTimeout(timer);
      const ok = code === 0 && fs.existsSync(output);
      resolve({ url, output, ok, code, error: ok ? '' : (stderr.trim() || `exit ${code}`) });
    });
  });
}

async function main() {
  const results = [];
  let next = 0;
  async function worker() {
    while (true) {
      const index = next++;
      if (index >= urls.length) return;
      const result = await runOne(urls[index], index);
      results.push(result);
      console.log(JSON.stringify(result));
    }
  }
  await Promise.all(Array.from({ length: Math.max(1, concurrency) }, worker));
  fs.writeFileSync(path.join(outputDir, 'results.json'), JSON.stringify(results, null, 2));
  process.exitCode = results.every(r => r.ok) ? 0 : 1;
}
main().catch(error => { console.error(error); process.exitCode = 1; });

Run the batch

  1. Create urls.txt, one absolute URL per line.
  2. Run node batch-screenshots.js urls.txt shots.
  3. Set a different executable with PHANTOMJS=/opt/phantomjs/bin/phantomjs.
  4. Tune the example limit with CONCURRENCY=2 or another value. No universal safe number is documented; measure memory, CPU, network load, and failure rate on your machine.
  5. Inspect shots/results.json; a job is successful only when PhantomJS exits 0 and the expected file exists.

Capture dimensions and formats

Viewport versus crop

viewportSize establishes the layout viewport, so responsive pages may render a different navigation or column arrangement at 1280 pixels than at a phone width. clipRect crops the saved result to a rectangle. It does not make the page responsive; change the viewport when you need a different breakpoint.

Output files

Use extensions such as .png, .jpg, .gif, or .pdf according to the formats supported by your installed PhantomJS build. Keep one deterministic output path per URL. The hash-based name in the controller prevents query strings, slashes, and duplicate hostnames from overwriting one another.

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

Reliability practices for real batches

  • Keep input identity: store the original URL, output path, exit code, load status, and stderr in a manifest.
  • Reject stale files: check the child’s exit code and verify the file was created during this run; an old file must not count as a new screenshot.
  • Use timeouts: a page can wait forever on a resource. Kill the child and mark that URL failed rather than blocking the entire batch.
  • Retry selectively: retry transient network failures, but do not loop indefinitely on a page that consistently returns an unsuccessful page.open status.
  • Protect resources: separate processes consume memory. Increase concurrency only after observing the host under the intended workload.
  • Quote safely: pass arguments as the array given to spawn; do not concatenate untrusted URLs into a shell command.

Troubleshooting

“phantomjs: command not found”

Install or unpack a compatible executable, add its directory to PATH, or set the PHANTOMJS environment variable to its absolute path. Confirm with phantomjs --version.

Every job returns an unsuccessful status

Test one URL directly: phantomjs capture.js https://example.com test.png. Check DNS, proxy and TLS support, then try a simpler page. Modern sites may depend on browser APIs that this legacy engine does not implement.

The process never finishes

Keep the controller timeout enabled. A page-open callback may be delayed by a stalled resource; terminate that child, record the timeout, and continue with the remaining URLs.

Images are blank or incomplete

Require status === 'success' before rendering, and consider a page-specific delay only if your script adds one. PhantomJS does not provide the feature coverage of current browsers, so client-side rendering or lazy content may never appear.

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

Files overwrite each other

Do not derive names from only the hostname. Include an index and a hash of the complete URL, as the controller does.

When a hosted renderer is a better fit

A local PhantomJS batch gives you process-level control and keeps files on your infrastructure, but you maintain the executable, compatibility workarounds, retries, and capacity. PhantomJSCloud documentation describes a Node.js client and batch requests for managed rendering; verify its current availability, limits, and pricing before choosing it.

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

Or skip the browser setup

ScreenshotNeo is a current website screenshot API and MCP server. It accepts one GET request, handles the browser, and returns PNG, JPEG, WebP, or PDF. Before capture it accepts cookie/consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing; response headers identify the page verdict and whether the shot was billed. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.

See the ScreenshotNeo API documentation for authentication and options. A minimal call is:

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

You can also use Python:

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)

Or Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
const fs = require('node:fs');
fs.writeFileSync('shot.webp', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo supports full-page lazy-image loading, CSS-selector element capture, device presets, arbitrary viewports, retina scale, PDF paper and page-range controls, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, configurable caching, signed image links, asynchronous webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification. Its parameter names also accept those used by other screenshot APIs, easing migration. Every plan includes every feature: 1,000 shots per month free with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account to try it.

Operational checklist

  • Pin and test the PhantomJS binary version.
  • Run one known-good URL before a large batch.
  • Choose viewport and crop dimensions deliberately.
  • Use bounded concurrency and a timeout.
  • Persist per-URL results and stderr.
  • Validate output files, not just process exit codes.
  • Plan a migration if modern browser compatibility becomes a requirement.

Frequently Asked Questions

Can PhantomJS take a full-page screenshot automatically?

The documented controls cover the viewport and an optional clip rectangle. A full-page result requires a script and dimensions that match your intended page; PhantomJS does not make modern full-page capture behavior automatic.

Can I run the controller on Windows?

Yes, if a compatible PhantomJS executable runs on that system. Use the executable’s full path in PHANTOMJS and test path permissions, process termination, and output writing before batching.

Why is a separate process preferable to a Node package?

PhantomJS is its own command-line application rather than a Node.js module. A child process preserves that supported integration boundary and gives Node explicit exit codes and stderr to manage.

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.