Running the Obscura browser on AWS Lambda
Obscura is a stealth Chromium fork that exposes a Chrome DevTools Protocol (CDP) endpoint, which makes it a drop-in target for Playwright or Puppeteer when you want a less obviously automated browser.
Getting it running on AWS Lambda was annoying enough that I wrote a small package to make it boring instead: node-obscura-aws-lambda (npm).
It is two things in one repo:
- a custom Obscura build, compiled against the AWS Lambda runtime
- a tiny Node wrapper that installs that binary and starts it
The result is that you can npm install it into a normal
Node Lambda function and call startObscura(). No custom
runtime, no Lambda layer juggling, no container image required.
Why this is needed
The upstream Obscura release binaries are built against a recent
Linux toolchain and dynamically link against
glibc 2.35+. AWS Lambda’s Node.js runtimes ship on
Amazon Linux 2023, which provides an older glibc. Trying to execute
the upstream binary inside a Lambda function results in the familiar
dynamic linker errors:
./obscura: /lib64/libc.so.6: version `GLIBC_2.35' not found
The fix is unglamorous: rebuild Obscura inside the same userspace Lambda actually runs on. That is the entire reason this package exists.
How the build works
The repo is Docker-first, so you do not need a local Rust toolchain.
One npm script clones upstream Obscura, builds it inside an
amazonlinux:2023 image, and packages
obscura and obscura-worker into a
reproducible tarball:
git clone https://github.com/chegger/node-obscura-aws-lambda
cd node-obscura-aws-lambda
npm run build:artifact
That produces dist/obscura-x86_64-linux-lambda.tar.gz
plus a SHA256 and a small build-metadata file. There is also a smoke
test that runs the binary inside the AWS Lambda Node 24 base image
and waits for /json/version to come up:
npm run smoke:test
The published package version (v0.1.0 at the time of
writing) maps to a GitHub release that hosts that tarball as a
download asset. The npm package itself stays small; the binary is
fetched on install.
How the wrapper works
The runtime side is intentionally small. On
npm install, a postinstall script downloads the matching
tarball, verifies the platform is Linux x64, extracts the binaries
into binaries/linux-x64-lambda/, and marks them
executable.
At runtime, startObscura() picks an open port, spawns
obscura serve --port <port>, polls
/json/version until the CDP endpoint is ready, and
returns the HTTP and WebSocket endpoints plus a
close() handle. The API is intentionally identical to
@chegger/node-obscura so you can swap one for the other.
Using it from a Lambda
Install:
npm install @chegger/node-obscura-aws-lambda playwright-core
Handler:
const { chromium } = require('playwright-core');
const { startObscura } = require('@chegger/node-obscura-aws-lambda');
exports.handler = async () => {
const obscura = await startObscura({ stealth: true });
const browser = await chromium.connectOverCDP(obscura.endpoint);
try {
const context = browser.contexts()[0] || (await browser.newContext());
const page = await context.newPage();
await page.goto('https://example.com');
return { title: await page.title() };
} finally {
await browser.close();
await obscura.close();
}
};
If you bundle the Lambda with
aws-cdk-lib/aws-lambda-nodejs, force Docker bundling so
the package’s Linux binary gets installed inside a Lambda-shaped
environment rather than on your local machine:
const myLambdaFunction = new lambda.NodejsFunction(this, 'MyLambdaFunction', {
// ...
runtime: l.Runtime.NODEJS_24_X,
architecture: l.Architecture.X86_64,
bundling: {
forceDockerBundling: true,
nodeModules: ['@chegger/node-obscura-aws-lambda'],
},
});
A few practical notes for Lambda:
-
Use the
nodejs24.xruntime, x86_64. The published binary is built for that base image. arm64 is not supported yet. -
Bundle the package’s
node_modulesinto your deployment artifact (zip or container) afternpm installhas run on a Linux x64 host. The binary is downloaded by postinstall and must end up inside the artifact. For CDKNodejsFunction, that usually means settingbundling.forceDockerBundling = true. - Give the function enough memory (1024–2048 MB is a sane starting point) and a timeout that fits your workload. Headless browsers are not free.
-
Cold starts get noticeably better if you keep the
startObscura()result around between invocations inside the same container, similar to any other browser-on-Lambda setup. -
If your build host has no internet access, set
NODE_OBSCURA_AWS_LAMBDA_SKIP_DOWNLOAD=1during install and supply the binary another way.
Customising it
A few environment variables let you point the build and install at different sources, which is useful if you want to pin a specific upstream Obscura tag or host the binary somewhere private:
-
OBSCURA_UPSTREAM_REPO,OBSCURA_UPSTREAM_TAG,OBSCURA_CARGO_FEATUREScontrol the build. -
OBSCURA_AWS_LAMBDA_RELEASE_TAGandOBSCURA_AWS_LAMBDA_DOWNLOAD_BASE_URLcontrol where postinstall fetches the binary from.
Why publish it
Rebuilding a stealth browser against an old glibc is not interesting
work. Doing it once, pinning it to a Lambda-shaped runtime, and
publishing both the artifact and a small wrapper means the next
person (often a future me) can just
npm install and move on.
If you run into issues, please open one on
GitHub.
Patches that add arm64 support or a precompiled
nodejs22.x variant are very welcome.