Skip to content
Scraping API · Use case

Run Puppeteer as a service, with no Chrome to install or scale

Puppeteer as a service means you keep writing Puppeteer and stop running Chrome. Scrapers that need clicks, QA checks, data pulls from interactive pages and one-off automations all need a browser only for the seconds the script runs. With Browser Functions you send the function with a URL and get its return value back.

The problem

Headless Chrome is the heaviest dependency in a serverless stack

A Chromium build is too large for most function bundles, needs system fonts and libraries the runtime does not ship, and cold-starts slowly. On a server it leaks memory, leaves zombie processes behind after a crash and has to be upgraded in step with Puppeteer.

Serverless Puppeteer workarounds trade one problem for another: trimmed Chromium builds pinned to old versions, layers that break on the next runtime upgrade, or a browser pool you now size, monitor and restart. None of it is the script you actually wanted to run.

The function method moves the browser behind the request. When your function references page, Microlink starts a headless browser, navigates to the URL and calls your function with the full Puppeteer Page. It returns whatever your function returns, plus profiling, and the browser is destroyed afterwards.

How it works

How to run Puppeteer as a service with microlink.function

Write the function as you would locally, but receive page instead of launching a browser. The navigation to the URL has already happened when your code starts.

1 · Your Puppeteer code, remote
import createClient from 'microlink.io'

const microlink = createClient({
  apiKey: process.env.MICROLINK_API_KEY
})

const { isFulfilled, value } = await microlink.function(
  'https://news.ycombinator.com',
  ({ page }) =>
    page.$$eval('.titleline > a', links =>
      links.slice(0, 5).map(a => ({ title: a.textContent, url: a.href }))
    )
)

The function receives page after navigation, along with url, headers and response. value is the array your function returned, and isFulfilled tells you it completed without throwing.

2 · Prepare the page, pass your own arguments
import createClient from 'microlink.io'

const microlink = createClient({
  apiKey: process.env.MICROLINK_API_KEY
})

const { value } = await microlink.function(
  'https://example.com/pricing',
  ({ page, selector }) => page.$$eval(selector, rows => rows.map(row => row.innerText)),
  { waitForSelector: '.plan', selector: '.plan' }
)

API options such as waitForSelector, click or scripts prepare the page before your code runs. Any option that is not an API parameter, like selector here, arrives as a named argument.

3 · The same function over HTTP
curl 'https://api.microlink.io/?url=https%3A%2F%2Fexample.com&function=%28%7B+page+%7D%29+%3D%3E+page.title%28%29&meta=false'

Without the SDK, send the function as a string in the function parameter. The result is under data.function with isFulfilled, value, profiling and logging.

Parameters used
  • function The JavaScript to run. The SDK serializes and compresses it for you.
  • waitForSelector Waits for an element before the function is called.
  • scripts Injects a library such as jQuery into the page before your code runs.
  • meta The SDK sends false by default so the request only pays for the function.
  • timeout The request ceiling: 30 seconds on free, 60 seconds on Pro.

Plan limits are explicit: on the free plan a function gets 15 seconds, 64 MB of heap, 1024 bytes of compressed code, one in-flight run per IP and same-origin requests only. Pro raises that to 60 seconds, 128 MB, unlimited code size and concurrency, and unrestricted outgoing requests. The profiling guide shows where each run spends its time.

Why it works

Why hosted headless Chrome beats a browser you maintain

The browser is infrastructure. The function is your product. Splitting them lets you ship the second without owning the first.

01 · Nothing to ship
No Chromium binary, no layer, no fonts.
Your code is a function, so it deploys anywhere Node.js, an edge runtime or a browser can make an HTTP call. The SDK compresses the function body before sending it, with brotli in Node.js and lz-string in browsers.

Try a function against a live page in the editor before wiring it into your code, and read the Browser Functions feature for the full runtime.

02 · Fails loudly
Errors come back as data, not crashes.
A function that throws resolves with isFulfilled false and the error name and message in value. Hitting a limit returns a named error such as TimeoutError or MemoryError whose message states the exact limit.

The function troubleshooting guide maps each error, including EINVALFUNCTION for syntax errors, to its fix.

03 · Clean every time
A fresh, isolated browser per call.
Each request gets its own browser that is destroyed when the response is sent. No cookies or storage carry over between runs, so one job can never see another job’s state.

When not to: private and loopback targets such as localhost are refused with EFORBIDDENURL, so this is not a way to test a dev server, and flows that need state across many requests or more than 60 seconds belong on a browser you run. See request isolation.

FAQ

Can I run my existing Puppeteer script as a service?

Mostly as is. Remove the browser launch and the page.goto, take page from the function arguments and return the result. Everything after navigation, from page.click to page.$$eval, works unchanged because page is a full Puppeteer Page.

How long can a serverless Puppeteer function run?

15 seconds on the free plan and up to 60 seconds on Pro. Past that the function returns isFulfilled false with a TimeoutError. Replace fixed waits with waitForSelector and set meta: false to stay well inside the limit.

Can the hosted headless Chrome reach localhost or a private network?

No. Private, loopback and link-local addresses are rejected with EFORBIDDENURL before the browser starts. The target must be a public URL.

What happens when my Puppeteer function throws an error?

The promise still resolves. isFulfilled is false and value holds the error name and message. The promise only rejects when the API call itself fails, such as an invalid URL or an expired key.

Can a Puppeteer function use npm packages?

Yes. require() any package inside the function and it is installed on the fly and cached for later runs. Run JavaScript with npm packages covers version pinning and the sandbox restrictions.
Related use cases

Solve the next problem with the same API

JavaScript with npm packages

Require any npm package inside a remote function, pin its version, and skip the browser entirely when the code does not need one.

Pagination and Load more buttons

Scrape numbered pages in parallel, one call per page, or click Load more inside a function until the list is complete.

Custom fields from JavaScript apps

Render React, Vue or Angular apps in a real browser, wait for the element you need, then run your rules.

Scraping behind a login

Forward a session cookie or bearer token as a request header and extract data from pages only your users can see.

Screenshots of JavaScript-rendered pages

Wait for a selector, a lifecycle event or a delay so single-page apps and lazy sections finish rendering before capture.

PDFs of JavaScript-rendered pages

Print dashboards and single-page apps after they render: wait for the chart, open tabs and sections, then print.

Ready to run Puppeteer without Chrome?

Send a function, get a value. Start on the free tier and move your first script off your own browser today.