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.

The error means Node.js is interpreting PhantomJS code. webpage is PhantomJS’s built-in Web Page Module, not an npm package that Node can resolve. Run the file with the PhantomJS executable, or keep the Lambda handler in Node.js and use a Node-to-PhantomJS bridge (or a maintained browser automation runtime). A Lambda layer can package files, but it cannot change the interpreter or make PhantomJS built-ins available to Node.

Why require('webpage') fails in Lambda

PhantomJS documentation shows this pattern:

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

That statement is valid only when PhantomJS evaluates the file. PhantomJS supplies webpage internally. Node.js uses a different module resolver and looks for a Node package or a file in its dependency paths. Because no ordinary npm package provides PhantomJS’s built-in module, Node reports Cannot find module 'webpage'.

In Lambda, the usual trigger is one of these:

  • The function runtime is Node.js and the handler directly contains PhantomJS code.
  • A PhantomJS example is launched with node script.js instead of the phantomjs executable.
  • A Node bridge process is being used, but the code still calls require('webpage') instead of the bridge’s page API.
  • A layer or zip contains the files, but the handler is still running under Node. Packaging does not cross that runtime boundary.

Do not try to solve this by adding webpage to package.json. The fix is to choose which process owns the page: PhantomJS itself, or Node through a bridge/replacement API.

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

Choose the correct execution model

Approach What changes Packaging concern Best fit
Standalone PhantomJS child process Keep PhantomJS script semantics and invoke the executable explicitly. Native executable, libraries, permissions, architecture and process limits must all match Lambda. Existing PhantomJS scripts that you need to preserve.
Node bridge or replacement browser Rewrite page operations around a Node-facing API; remove PhantomJS-only imports from the handler. Node dependencies and the selected browser runtime must fit the Lambda runtime and architecture. New work or code you want to maintain in Node.

PhantomJS 2.1 was released on January 23, 2016 and uses Qt 5.5.1/WebKit. Treat it as legacy infrastructure: pin the exact binary, test the complete deployment artifact, and plan a migration to a maintained browser stack when your requirements permit.

Fix A: invoke the PhantomJS script as PhantomJS

1. Put PhantomJS code in its own file

For example, save this as capture.js. It reads a URL from PhantomJS’s argument list, opens the page, writes a screenshot, and exits with a useful status.

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

if (system.args.length < 2) {
  console.log('Usage: phantomjs capture.js URL [output-file]');
  phantom.exit(2);
}

var url = system.args[1];
var output = system.args[2] || '/tmp/shot.png';
var page = webPage.create();

page.viewportSize = { width: 1280, height: 800 };
page.open(url, function (status) {
  if (status !== 'success') {
    console.error('OPEN_FAILED ' + status);
    phantom.exit(1);
    return;
  }

  page.render(output);
  console.log('OK ' + output);
  phantom.exit(0);
});

The important detail is not the screenshot logic; it is that this file is consumed by PhantomJS. Do not import it from the Node handler and expect Node to provide webpage.

2. Spawn PhantomJS from the Node Lambda handler

The handler remains Node.js, while the child process interprets capture.js. The executable path below is an example placeholder; use the path created by your deployment package or layer.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const { spawn } = require('child_process');
const path = require('path');

exports.handler = async (event) => {
  const url = event.url;
  if (typeof url !== 'string' || !url) {
    throw new Error('event.url must be a non-empty string');
  }

  const phantom = process.env.PHANTOMJS_PATH || '/opt/bin/phantomjs';
  const script = path.join(__dirname, 'capture.js');
  const output = '/tmp/shot.png';

  return await new Promise((resolve, reject) => {
    const child = spawn(phantom, [script, url, output], {
      stdio: ['ignore', 'pipe', 'pipe']
    });

    let stdout = '';
    let stderr = '';
    child.stdout.on('data', chunk => { stdout += chunk; });
    child.stderr.on('data', chunk => { stderr += chunk; });

    const timer = setTimeout(() => {
      child.kill('SIGKILL');
      reject(new Error('PhantomJS timed out'));
    }, 85000);

    child.on('error', error => {
      clearTimeout(timer);
      reject(error);
    });

    child.on('close', code => {
      clearTimeout(timer);
      if (code !== 0) {
        reject(new Error(`PhantomJS exited ${code}: ${stderr || stdout}`));
        return;
      }
      resolve({ statusCode: 200, file: output, log: stdout });
    });
  });
};

Use a timeout below the Lambda function timeout so the handler can terminate the child and return a controlled error. Capture both streams and the exit code; PhantomJS can report a page-open failure through its own output even when the Node process itself started correctly. Store temporary output under /tmp, the writable location available to the function, and send the completed file to your chosen storage or response path.

3. Package the executable correctly

  • Place capture.js, the handler, and ordinary Node dependencies at the root of a zip deployment. Lambda expects the handler file at the archive root for this layout.
  • Ensure the PhantomJS executable and every required native library are present. Mark the executable and its containing directories with appropriate POSIX read and execute permissions.
  • Build or obtain the binary for the function’s architecture, either x86_64 or arm64, and for the selected Lambda runtime. A binary built for another architecture will fail before your script reaches require('webpage').
  • If using a layer, put Node dependencies under nodejs/node_modules or the runtime-specific nodejs/nodeXX/node_modules directory. Lambda extracts layer contents below /opt; adjust PHANTOMJS_PATH to the actual location.

Test the exact zip or container image, not just a local copy of the script. Native browser stacks often fail because a shared library, permission bit or architecture differs from the development machine.

Fix B: keep the handler in Node.js

If the function should remain a Node application, delete require('webpage') from code executed by the handler. A Node-to-PhantomJS bridge exposes a page object through its own documented API; it does not install PhantomJS’s internal modules into Node’s resolver. Rewrite calls such as page creation, navigation, rendering and event handling to match that bridge.

For new implementations, evaluate a currently maintained headless-browser solution instead of adding more code around the legacy PhantomJS 2.1 stack. The replacement still has to fit Lambda’s runtime, memory, timeout, binary and architecture constraints. Do not assume that changing the library name removes those deployment requirements.

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

Lambda packaging and resolution checklist

  1. Confirm the handler runtime. Check the function’s configured runtime and handler entry point. If it is Node.js, Node will parse the handler and resolve its imports.
  2. Inspect the archive. For a zip deployment, verify that the handler and project files are at the archive root, with node_modules alongside them.
  3. Verify a layer’s directory shape. Use nodejs/node_modules or the documented runtime-specific nodejs/nodeXX/node_modules path. A random directory under the layer will not be searched.
  4. Log Node’s search path. Temporarily log process.env.NODE_PATH and the resolved location of an ordinary Node dependency. This diagnoses Node package lookup; it cannot make webpage a Node module.
  5. Check permissions. Native executables must be readable and executable, and their parent directories must be traversable.
  6. Check architecture and native libraries. Match the binary and libraries to x86_64 or arm64 and to the selected runtime.
  7. Run the right command. A PhantomJS file must be launched with phantomjs capture.js ..., never node capture.js.

Troubleshooting common failures

“Cannot find module ‘webpage’” still appears

Look at the process that emitted the message. If the log is from Node, the PhantomJS file is still being loaded by the handler or a Node child process. Move it behind an explicit PhantomJS invocation, or replace the import with the bridge’s API.

“phantomjs: command not found” or an immediate spawn error

The executable path is wrong or the file is absent from the zip/layer. List the deployed directory during a diagnostic invocation, set PHANTOMJS_PATH to the real path, and verify that the file has execute permission.

Exec format error

The binary architecture does not match the Lambda function, or the file is not a valid executable for the selected runtime environment. Rebuild or replace it for the configured architecture and test the deployment artifact.

“Permission denied”

Fix POSIX permissions on the executable and every parent directory before creating the zip. Packaging from a filesystem that strips execute bits is a common cause.

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

The process starts but the page never opens

Capture PhantomJS stdout and stderr, inspect the exit code, and enforce a child-process timeout. The target may be unreachable, require browser features PhantomJS does not support, or trigger a bot check. A successful process launch does not prove that a page loaded.

The layer is present but Node dependencies are missing

Recheck the layer’s required nodejs/... directory structure and the function’s attached layers. Log process.env.NODE_PATH and resolve an ordinary package. Remember that a correctly shaped layer fixes Node package lookup only; it does not provide PhantomJS built-ins to Node.

Reliability, performance and cost considerations

  • Cold starts: Native browser files and libraries increase initialization work. Keep the package limited to what the selected model needs.
  • Timeouts: Set a browser timeout below the Lambda timeout, terminate hung children, and return an error that identifies the URL and process exit status without exposing secrets.
  • Concurrency: Each invocation can create a native process and consume memory. Set reserved or account concurrency deliberately and avoid assuming that a local single-process test predicts parallel behavior.
  • Temporary files: Use /tmp for intermediate screenshots and clean up or overwrite files so repeated invocations do not consume unnecessary space.
  • Network behavior: A page can fail because of DNS, outbound network configuration, TLS support or a site’s bot defenses. Distinguish those outcomes from module-resolution errors in logs.
  • Maintenance: PhantomJS 2.1’s age means modern JavaScript, TLS and browser behavior may be incompatible. Pin versions and test representative pages before relying on it in production.
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 provides a website screenshot API and MCP server, so you can make one request instead of packaging PhantomJS, Qt libraries and a browser process. Before capture it accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups and chat widgets; each cleanup step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers.

For a direct HTTP call, see the ScreenshotNeo API documentation:

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

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)

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

ScreenshotNeo also supports full-page captures with lazy images, CSS-selector element shots, dark mode, 12 device presets or custom viewports, retina scale, PDF output, custom CSS and JavaScript, clicks before capture, selector or network-idle waits, request and resource blocking, custom headers/cookies/user agents, authorization, timezone and geolocation, transparent backgrounds, resizing, configurable caching, signed image links, asynchronous jobs with signed webhooks, bulk capture for 100 URLs per call, a usage API and an OpenAPI specification. Its MCP server exposes take_screenshot, get_page_info and capture_pdf to Claude, Cursor and other MCP clients.

The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots; every feature is available on every plan, and yearly billing gives two months free. Create a free ScreenshotNeo account to try the API without configuring a Lambda browser runtime.

FAQ

Does adding a Lambda layer make PhantomJS modules visible to Node?

No. A layer changes where files are mounted and how ordinary dependencies are found. It does not change the interpreter that executes a JavaScript file.

Can one Lambda invocation use both Node and PhantomJS?

Yes. Node can launch PhantomJS as a child process, exchange arguments and output, and then continue handling the request. The two runtimes remain separate.

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

Why does the same source work locally?

Your local command may be invoking PhantomJS while Lambda invokes Node, or your local machine may have libraries and permissions absent from the deployment artifact. Compare the actual executable, command, architecture and packaged files.

Should a new project still choose PhantomJS?

Usually treat it as a compatibility obligation rather than a default. Its 2016-era WebKit stack can require legacy workarounds, so select a maintained browser runtime when your page and Lambda constraints allow.

Frequently Asked Questions

Can one Lambda invocation use both Node and PhantomJS?

Yes. Node can launch PhantomJS as a child process, exchange arguments and output, and then continue handling the request. The two runtimes remain separate.

Why does the same source work locally?

Your local command may invoke PhantomJS while Lambda invokes Node, or your local machine may have libraries and permissions absent from the deployment artifact. Compare the executable, command, architecture and packaged files.

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

Should a new project still choose PhantomJS?

Usually treat it as a compatibility obligation rather than a default. Its 2016-era WebKit stack can require legacy workarounds, so select a maintained browser runtime when your page and Lambda constraints allow.

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.