Serverless Screenshot API

Use a screenshot API from Lambda, Vercel, Workers, or any serverless function. One HTTP call replaces headless Chromium cold starts, deployment layers, and container management.

Vitalii Holben Updated

Serverless functions have a timeout, a memory cap, and no persistent filesystem. Chrome has none of those constraints in mind. Running headless Chromium on Lambda or Cloud Functions means fighting cold starts, managing a binary that unpacks to 180+ MB at runtime, and debugging crashes that only happen under concurrent load. A screenshot API replaces that entire stack with one outbound HTTPS request that fits inside any serverless runtime, any timeout, and any memory limit.

What do you give up? Control. Self-hosted Chromium lets you customize the browser process, inject extensions, and control every rendering detail. The API gives you the screenshot with less code, less infrastructure, and fewer 3 AM pages about Lambda running out of /tmp space.

AWS Lambda: the most common case

Lambda is where most teams first try running Puppeteer, and where most teams give up. The @sparticuz/chromium package (successor to the abandoned chrome-aws-lambda) compresses Chromium into a Lambda layer, but the compressed binary still runs 50+ MB and decompresses to ~180 MB at runtime. Add your Node.js function code and you're close to Lambda's 250 MB deployment limit before writing any business logic.

Calling the API from Lambda is different:

// Lambda handler — screenshot via API
export const handler = async (event) => {
    const url = event.queryStringParameters?.url;
    if (!url) return { statusCode: 400, body: 'Missing url parameter' };

    const params = new URLSearchParams({
        url,
        format: 'webp',
        full_page: 'true',
        block_cookies: 'true',
        response_type: 'json',
    });

    const res = await fetch(
        `https://api.screenshotrun.com/v1/screenshots/capture?${params}`,
        { headers: { Authorization: `Bearer ${process.env.SCREENSHOTRUN_KEY}` } }
    );

    const data = await res.json();

    return {
        statusCode: 200,
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            screenshot_url: data.screenshot_url,
            metadata: data.metadata,
        }),
    };
};

No Chromium layer, no 4-second cold start, no memory tuning. The function runs in 128 MB, deploys in seconds, and the only dependency is the runtime's built-in fetch. Lambda's default 3-second timeout is tight for complex pages, so bump it to 15-30 seconds (the API typically returns in 2-8 seconds depending on page complexity).

For pages that take longer (SPAs with heavy client-side rendering, full-page captures of long pages), use the webhook parameter instead. Fire the request with webhook_url pointing at an API Gateway endpoint, and the Lambda function returns immediately. A second Lambda processes the screenshot when the webhook fires. No timeout pressure at all.

Vercel and Next.js API routes

Vercel's serverless functions have a 10-second timeout on the Hobby plan and 60 seconds on Pro. Plenty of teams deploy @vercel/og for simple OG images, but when you need to screenshot an actual URL (not just render a React component), you need a browser. And Vercel functions don't support running Chrome.

// app/api/screenshot/route.ts (Next.js App Router)
import { NextRequest, NextResponse } from 'next/server';

export async function GET(req: NextRequest) {
    const url = req.nextUrl.searchParams.get('url');
    if (!url) return NextResponse.json({ error: 'url required' }, { status: 400 });

    const params = new URLSearchParams({
        url,
        format: 'png',
        width: '1200',
        height: '630',
        block_cookies: 'true',
        block_ads: 'true',
        response_type: 'image',
    });

    const res = await fetch(
        `https://api.screenshotrun.com/v1/screenshots/capture?${params}`,
        { headers: { Authorization: `Bearer ${process.env.SCREENSHOTRUN_KEY}` } }
    );

    if (!res.ok) return NextResponse.json({ error: 'capture failed' }, { status: 502 });

    return new NextResponse(res.body, {
        headers: {
            'Content-Type': 'image/png',
            'Cache-Control': 'public, max-age=86400',
        },
    });
}

This streams the screenshot directly from the API to the client (no intermediate storage needed). Set Cache-Control headers and Vercel's CDN caches the result at the edge, so subsequent requests for the same URL serve from cache without hitting the API again. For OG image generation, this pattern means your <meta property="og:image"> tag points at /api/screenshot?url=... and social platforms get a fresh screenshot on first crawl, cached from then on.

Cloudflare Workers: no Chrome, period

Workers run on V8 isolates with a 128 MB memory limit and no filesystem access. You can't run Chromium here. Cloudflare's own Browser Rendering API exists, but it's a separate product with its own pricing and a limited feature set (our comparison page covers the differences). Calling the screenshot API from a Worker takes about 15 lines of code:

// Cloudflare Worker
export default {
    async fetch(request) {
        const { searchParams } = new URL(request.url);
        const url = searchParams.get('url');
        if (!url) return new Response('Missing url', { status: 400 });

        const params = new URLSearchParams({
            url,
            format: 'webp',
            width: '1280',
            block_cookies: 'true',
            response_type: 'image',
        });

        const screenshot = await fetch(
            `https://api.screenshotrun.com/v1/screenshots/capture?${params}`,
            { headers: { Authorization: `Bearer ${SCREENSHOTRUN_KEY}` } }
        );

        return new Response(screenshot.body, {
            headers: {
                'Content-Type': 'image/webp',
                'Cache-Control': 'public, max-age=3600',
            },
        });
    },
};

Workers have a 30-second CPU time limit (not wall time), so the API call's network wait doesn't count against your compute budget. The response streams back through Cloudflare's network, which means your screenshots are served from 300+ edge locations without any extra CDN setup.

Google Cloud Functions and Cloud Run

Cloud Functions supports longer timeouts (up to 9 minutes on 2nd gen) and more memory (up to 32 GB), so running Puppeteer is technically possible here. But you still deal with the Chromium deployment complexity: building a container image with Chrome, managing font packages for proper text rendering (Linux renders fonts differently from macOS, and missing system fonts show up as tofu blocks), and handling the --no-sandbox flag that some Cloud Run configurations require.

Same call pattern as Lambda. Python works well with Cloud Functions since most ML and data pipelines are Python-first:

import functions_framework
import httpx

@functions_framework.http
def screenshot(request):
    url = request.args.get("url")
    if not url:
        return ("Missing url parameter", 400)

    resp = httpx.get(
        "https://api.screenshotrun.com/v1/screenshots/capture",
        params={
            "url": url,
            "format": "webp",
            "full_page": "true",
            "block_cookies": "true",
            "extract_metadata": "true",
            "response_type": "json",
        },
        headers={"Authorization": f"Bearer {SCREENSHOTRUN_KEY}"},
        timeout=30,
    )

    return resp.json()

If you're building a pipeline that captures screenshots as part of a larger workflow (data enrichment, content aggregation, visual archiving), the function calls the API and stores the result in Cloud Storage or feeds it into the next pipeline step. No browser process to manage between invocations.

The timeout problem and how webhooks solve it

Every serverless platform has a wall clock limit. Lambda defaults to 3 seconds (configurable to 15 minutes). Vercel Hobby: 10 seconds. Workers: 30 seconds CPU. Most screenshot captures complete in 2-8 seconds. But heavy pages (SPAs with long hydration, sites behind bot detection that trigger challenge pages, full-page captures of very long pages) can take 15-30 seconds.

Two options when you hit the timeout wall:

First, bump the timeout. Lambda and Cloud Functions both support longer execution times. This costs more (Lambda charges per 100ms of runtime, so a 15-second function costs 5x a 3-second one), but it's the simplest fix.

Second, go async with webhooks. Send the capture request with webhook_url pointing at your endpoint. The API returns immediately with a job ID. When the screenshot is ready, the webhook fires with the result. Your serverless function never blocks:

// Step 1: Dispatch capture (returns instantly)
const dispatch = await fetch(
    `https://api.screenshotrun.com/v1/screenshots/capture?${new URLSearchParams({
        url: targetUrl,
        format: 'png',
        full_page: 'true',
        webhook_url: 'https://your-app.com/api/webhook/screenshot',
    })}`,
    { headers: { Authorization: `Bearer ${API_KEY}` } }
);

// Step 2: Separate function handles the webhook callback
export async function POST(req) {
    const payload = await req.json();
    // payload contains screenshot_url, metadata, certification
    await saveToDatabase(payload);
    return new Response('OK');
}

Webhooks work on every serverless platform, including Workers where the timeout is tight. It also decouples your capture pipeline from your request/response cycle, which matters when you're capturing dozens of URLs in parallel (fire 50 capture requests, process 50 webhook callbacks).

Cold starts and the screenshot latency budget

With self-hosted Chromium on Lambda, cold starts add 2-4 seconds on top of the actual capture time. That's Chrome decompressing, initializing, and launching. A warm function skips this, but Lambda recycles idle instances after 5-15 minutes of inactivity, so cold starts happen more than you'd expect in production. Not ideal for a screenshot service that needs consistent response times.

With the API, cold starts are your function's cold start only (typically 100-500ms for Node.js on Lambda, near zero on Workers). The API's rendering infrastructure is always warm because it serves thousands of concurrent requests. Your p50 capture latency drops from 6-8 seconds (self-hosted cold) to 2-4 seconds (API only), and p95 improves even more since you've eliminated the Chromium initialization variability.

Does latency matter? For user-facing features like link previews or OG image generation, absolutely. Caching is the real optimization. Return screenshots with Cache-Control headers and let your CDN (Cloudflare, Vercel, CloudFront) serve repeat requests from edge cache. The API also supports cache_ttl on its side, so identical requests within the TTL window return the cached result without re-rendering.

What about edge functions?

Edge functions (Vercel Edge Functions, Cloudflare Workers, Deno Deploy, Netlify Edge Functions) run in V8 isolates with even stricter constraints than traditional serverless. No filesystem, limited memory, restricted API surface. Running Chrome here isn't even a theoretical option.

But calling an external API is exactly what edge functions are designed for. The screenshot API is just another HTTP endpoint from the edge function's perspective. And since your edge function runs close to the end user, does the API benefit from that proximity? It depends on the API's routing (our servers accept requests globally and route to the nearest rendering cluster).

One thing to watch: edge functions on some platforms have payload size limits. Vercel Edge Functions cap the response body at 4 MB. A retina full-page screenshot in PNG can exceed that. Use response_type=json to get a URL instead of the binary image, or use WebP format (typically 3-5x smaller than PNG) to stay under the limit.

Cost math: API calls vs self-hosted Chromium

Lambda pricing: $0.0000166667 per GB-second. A Puppeteer screenshot that runs for 8 seconds at 2 GB memory costs $0.000267 per capture. Sounds cheap until you add the hidden costs: the Chrome layer deployment, the CI/CD pipeline to keep it updated, the monitoring for OOM crashes, the developer time debugging Lambda-specific Chrome quirks. I've seen teams spend 20+ hours per quarter maintaining their Lambda screenshot setup.

API pricing: $0.003-$0.009 per capture depending on plan. Ten to thirty times more per screenshot in raw compute cost. But you're buying no maintenance burden, no cold start optimization, no Chrome version management, and every rendering feature (stealth, geolocation, certified timestamps, full-page scrolling, consent banner removal) ships as URL parameters, not npm packages.

Break-even depends on volume and engineer cost. At 5,000 captures per month, Lambda compute runs roughly $1.33. The API costs $9-45 depending on plan. At 50,000 captures, Lambda compute is $13.33 and the API is $29-99. In both cases, the API's total cost of ownership is lower when you account for engineering time. Where self-hosting wins is above 100,000 captures per month with a team that already maintains Chromium infrastructure for other purposes (visual testing, PDF generation, scraping).

When to self-host Chromium instead

The API doesn't replace every serverless Chromium use case. Self-hosting makes sense when you need to run custom browser extensions, execute multi-step browser automation (fill forms, click through wizards, scrape data alongside screenshots), or capture pages that require session state accumulated over multiple navigations. Our Puppeteer vs API comparison and Playwright comparison cover these tradeoffs in detail.

For screenshots specifically (load URL, capture, return image), the API handles 95% of cases. The remaining 5% typically involve multi-step authenticated flows or custom browser profiles that the API's cookies and headers parameters can't replicate.

One pattern that works well: use the API for production screenshots and Puppeteer for local development and testing. Your CI runs visual regression tests with Playwright (full browser control), and your production app calls the API (zero infrastructure). Our Browserless comparison covers this hybrid approach.

Why ScreenshotRun for serverless apps

The API was built for exactly this: a single HTTP call that returns a screenshot. No SDKs to install, no Chrome binaries to manage, no container layers to maintain. It works from Lambda, Workers, Vercel, Cloud Functions, Deno Deploy, or any environment that can make an outbound HTTPS request. Every feature (full-page scrolling, consent banner removal, stealth mode, certified timestamps, geolocation) is available through URL parameters, not library imports.

The free plan gives you 200 captures per month to test the integration. For serverless use cases like link previews, OG image generation, and website thumbnails, that's enough to validate the pattern before scaling up. Integration guides for Node.js and Python cover the full API surface.

Written by

Vitalii Holben

Co-founder and CEO of ScreenshotRun. Full-stack developer building screenshot API tools that help developers capture, automate, and monitor web pages at scale. Based in Europe, shipping code since 201...

Screenshot API in a few clicks

Free plan includes 200 screenshots every month. No credit card required.

Sign up → copy your key → make your first call