{
  "version": "riddle-proof.profile-result.v1",
  "profile_name": "riddle-site-v491-serverless-simple-screenshot-response",
  "runner": "riddle",
  "status": "passed",
  "baseline_policy": "invariant_only",
  "route": {
    "requested": "https://riddledc.com/serverless/",
    "observed": "/serverless/",
    "expected_path": "/serverless/",
    "matched": true,
    "http_status": 200
  },
  "artifacts": {
    "screenshots": [
      "riddle-site-v491-serverless-simple-screenshot-response-desktop",
      "riddle-site-v491-serverless-simple-screenshot-response-phone",
      "riddle-site-v491-serverless-simple-screenshot-response-ipad-mini",
      "riddle-site-v491-serverless-simple-screenshot-response-ipad"
    ],
    "console": "console.json",
    "proof_json": "proof.json",
    "dom_summary": "dom-summary.json"
  },
  "checks": [
    {
      "type": "route_loaded",
      "label": "route_loaded",
      "status": "passed",
      "evidence": {
        "expected_path": "/serverless/",
        "observed_paths": [
          "/serverless/",
          "/serverless/",
          "/serverless/",
          "/serverless/"
        ],
        "http_statuses": [
          200,
          200,
          200,
          200
        ]
      }
    },
    {
      "type": "selector_visible",
      "label": "selector_visible",
      "status": "passed",
      "evidence": {
        "selector": ".landing-page",
        "visible_counts": [
          1,
          1,
          1,
          1
        ]
      }
    },
    {
      "type": "text_visible",
      "label": "text_visible",
      "status": "passed",
      "evidence": {
        "text": "Playwright for Serverless",
        "matches": [
          true,
          true,
          true,
          true
        ]
      }
    },
    {
      "type": "text_visible",
      "label": "text_visible",
      "status": "passed",
      "evidence": {
        "text": "After: One API call",
        "matches": [
          true,
          true,
          true,
          true
        ]
      }
    },
    {
      "type": "text_visible",
      "label": "text_visible",
      "status": "passed",
      "evidence": {
        "text": "POST a URL, get a PNG",
        "matches": [
          true,
          true,
          true,
          true
        ]
      }
    },
    {
      "type": "selector_text_visible",
      "label": "selector_text_visible",
      "status": "passed",
      "evidence": {
        "selector": ".code-block-wrapper",
        "text": "response.arrayBuffer()",
        "pattern": null,
        "viewports": [
          {
            "viewport": "desktop",
            "selector_count": 2,
            "visible_count": 2,
            "matched_count": 1,
            "matched": true,
            "samples": [
              "Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stri"
            ]
          },
          {
            "viewport": "phone",
            "selector_count": 2,
            "visible_count": 2,
            "matched_count": 1,
            "matched": true,
            "samples": [
              "Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stri"
            ]
          },
          {
            "viewport": "ipad-mini",
            "selector_count": 2,
            "visible_count": 2,
            "matched_count": 1,
            "matched": true,
            "samples": [
              "Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stri"
            ]
          },
          {
            "viewport": "ipad",
            "selector_count": 2,
            "visible_count": 2,
            "matched_count": 1,
            "matched": true,
            "samples": [
              "Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stri"
            ]
          }
        ]
      }
    },
    {
      "type": "selector_text_absent",
      "label": "selector_text_absent",
      "status": "passed",
      "evidence": {
        "selector": ".landing-page",
        "text": "const { job_id } = await response.json();",
        "pattern": null,
        "viewports": [
          {
            "viewport": "desktop",
            "selector_count": 1,
            "visible_count": 1,
            "matched_count": 0,
            "matched": false,
            "samples": []
          },
          {
            "viewport": "phone",
            "selector_count": 1,
            "visible_count": 1,
            "matched_count": 0,
            "matched": false,
            "samples": []
          },
          {
            "viewport": "ipad-mini",
            "selector_count": 1,
            "visible_count": 1,
            "matched_count": 0,
            "matched": false,
            "samples": []
          },
          {
            "viewport": "ipad",
            "selector_count": 1,
            "visible_count": 1,
            "matched_count": 0,
            "matched": false,
            "samples": []
          }
        ]
      }
    },
    {
      "type": "selector_text_absent",
      "label": "selector_text_absent",
      "status": "passed",
      "evidence": {
        "selector": ".landing-page",
        "text": "Poll for completion, get your screenshot",
        "pattern": null,
        "viewports": [
          {
            "viewport": "desktop",
            "selector_count": 1,
            "visible_count": 1,
            "matched_count": 0,
            "matched": false,
            "samples": []
          },
          {
            "viewport": "phone",
            "selector_count": 1,
            "visible_count": 1,
            "matched_count": 0,
            "matched": false,
            "samples": []
          },
          {
            "viewport": "ipad-mini",
            "selector_count": 1,
            "visible_count": 1,
            "matched_count": 0,
            "matched": false,
            "samples": []
          },
          {
            "viewport": "ipad",
            "selector_count": 1,
            "visible_count": 1,
            "matched_count": 0,
            "matched": false,
            "samples": []
          }
        ]
      }
    },
    {
      "type": "selector_text_absent",
      "label": "selector_text_absent",
      "status": "passed",
      "evidence": {
        "selector": ".landing-page",
        "text": "PNG delivered to S3. Poll for completion or use webhooks. Download when ready.",
        "pattern": null,
        "viewports": [
          {
            "viewport": "desktop",
            "selector_count": 1,
            "visible_count": 1,
            "matched_count": 0,
            "matched": false,
            "samples": []
          },
          {
            "viewport": "phone",
            "selector_count": 1,
            "visible_count": 1,
            "matched_count": 0,
            "matched": false,
            "samples": []
          },
          {
            "viewport": "ipad-mini",
            "selector_count": 1,
            "visible_count": 1,
            "matched_count": 0,
            "matched": false,
            "samples": []
          },
          {
            "viewport": "ipad",
            "selector_count": 1,
            "visible_count": 1,
            "matched_count": 0,
            "matched": false,
            "samples": []
          }
        ]
      }
    },
    {
      "type": "link_status",
      "label": "link_status",
      "status": "passed",
      "evidence": {
        "selector": "a[href*='/register/'], a[href*='/docs/'], a[href*='/pricing/'], a[href*='/vs-self-hosted/']",
        "expected_count": null,
        "min_count": 4,
        "allowed_statuses": [
          "2xx",
          "3xx"
        ],
        "require_nonzero_bytes": true,
        "min_bytes": 1000,
        "allowed_content_types": [
          "text/html"
        ],
        "viewports": [
          {
            "viewport": "desktop",
            "selector": "a[href*='/register/'], a[href*='/docs/'], a[href*='/pricing/'], a[href*='/vs-self-hosted/']",
            "total_count": 4,
            "discovered_count": 12,
            "ok_count": 4,
            "failed_count": 0,
            "truncated": false,
            "max_links": 20,
            "result_count": 4,
            "stored_result_count": 4,
            "omitted_result_count": 0,
            "omitted_success_count": 0,
            "results_compacted": false,
            "min_bytes": 1000,
            "allowed_content_types": [
              "text/html"
            ],
            "status_counts": {
              "206": 4
            },
            "failures": []
          },
          {
            "viewport": "phone",
            "selector": "a[href*='/register/'], a[href*='/docs/'], a[href*='/pricing/'], a[href*='/vs-self-hosted/']",
            "total_count": 4,
            "discovered_count": 12,
            "ok_count": 4,
            "failed_count": 0,
            "truncated": false,
            "max_links": 20,
            "result_count": 4,
            "stored_result_count": 4,
            "omitted_result_count": 0,
            "omitted_success_count": 0,
            "results_compacted": false,
            "min_bytes": 1000,
            "allowed_content_types": [
              "text/html"
            ],
            "status_counts": {
              "206": 4
            },
            "failures": []
          },
          {
            "viewport": "ipad-mini",
            "selector": "a[href*='/register/'], a[href*='/docs/'], a[href*='/pricing/'], a[href*='/vs-self-hosted/']",
            "total_count": 4,
            "discovered_count": 12,
            "ok_count": 4,
            "failed_count": 0,
            "truncated": false,
            "max_links": 20,
            "result_count": 4,
            "stored_result_count": 4,
            "omitted_result_count": 0,
            "omitted_success_count": 0,
            "results_compacted": false,
            "min_bytes": 1000,
            "allowed_content_types": [
              "text/html"
            ],
            "status_counts": {
              "206": 4
            },
            "failures": []
          },
          {
            "viewport": "ipad",
            "selector": "a[href*='/register/'], a[href*='/docs/'], a[href*='/pricing/'], a[href*='/vs-self-hosted/']",
            "total_count": 4,
            "discovered_count": 12,
            "ok_count": 4,
            "failed_count": 0,
            "truncated": false,
            "max_links": 20,
            "result_count": 4,
            "stored_result_count": 4,
            "omitted_result_count": 0,
            "omitted_success_count": 0,
            "results_compacted": false,
            "min_bytes": 1000,
            "allowed_content_types": [
              "text/html"
            ],
            "status_counts": {
              "206": 4
            },
            "failures": []
          }
        ],
        "failures": []
      }
    },
    {
      "type": "no_horizontal_overflow",
      "label": "no_horizontal_overflow",
      "status": "passed",
      "evidence": {
        "max_overflow_px": 1,
        "overflow_px": [
          0,
          0,
          0,
          0
        ],
        "bounds_overflow_px": [
          0,
          0,
          0,
          0
        ],
        "overflow_offender_counts": [
          0,
          0,
          0,
          0
        ],
        "viewports": [
          "desktop",
          "phone",
          "ipad-mini",
          "ipad"
        ]
      }
    },
    {
      "type": "no_fatal_console_errors",
      "label": "no_fatal_console_errors",
      "status": "passed",
      "evidence": {
        "console_fatal_count": 0,
        "page_error_count": 0,
        "total_console_fatal_count": 0,
        "total_page_error_count": 0,
        "allowed_console_fatal_count": 0,
        "explicitly_allowed_console_fatal_count": 0,
        "allowed_expected_network_mock_console_count": 0,
        "allowed_expected_network_mock_console_events": [],
        "allowed_page_error_count": 0,
        "allowed_console_texts": [],
        "allowed_console_patterns": [],
        "allowed_page_error_texts": [],
        "allowed_page_error_patterns": []
      }
    },
    {
      "type": "no_console_warnings",
      "label": "no_console_warnings",
      "status": "passed",
      "evidence": {
        "console_warning_count": 0,
        "total_console_warning_count": 0,
        "allowed_console_warning_count": 0,
        "allowed_console_texts": [],
        "allowed_console_patterns": [],
        "unallowed_console_warning_samples": [],
        "allowed_console_warning_samples": []
      }
    }
  ],
  "summary": "riddle-site-v491-serverless-simple-screenshot-response passed 13 check(s) across 4 viewport(s) (desktop, phone, ipad-mini, ipad).",
  "captured_at": "2026-05-17T12:43:27.683Z",
  "evidence": {
    "version": "riddle-proof.profile-evidence.v1",
    "profile_name": "riddle-site-v491-serverless-simple-screenshot-response",
    "target_url": "https://riddledc.com/serverless/",
    "baseline_policy": "invariant_only",
    "captured_at": "2026-05-17T12:43:27.683Z",
    "viewports": [
      {
        "name": "desktop",
        "width": 1280,
        "height": 900,
        "url": "https://riddledc.com/serverless/",
        "route": {
          "requested": "https://riddledc.com/serverless/",
          "observed": "/serverless/",
          "expected_path": "/serverless/",
          "matched": true,
          "http_status": 200
        },
        "title": "Serverless Browser Automation - Riddle",
        "body_text_length": 6065,
        "body_text_sample": "Skip to main content Riddle Docs Proof MCP Pricing Blog Playground Sign Up Log In Playwright for Serverless Chromium is ~280MB. Lambda's limit is 250MB. Vercel caps at 50MB. Cloudflare Workers can't run it at all. We solved all of these. Recognize These? Unzipped size must be smaller than 262144000 bytes Chromium is ~280MB. Lambda's unzipped limit is 250MB. The math doesn't work, no matter how clever you get with compression. spawn ENOMEM Lambda ran out of memory trying to spawn Chrome. You need 512MB minimum, 1-2GB recommended. That gets expensive. error while loading shared libraries: libnss3.so Missing Linux dependencies on Amazon Linux. Time to debug Lambda layers and figure out which .so files are missing. Task timed out after 30 seconds Cold start + Brotli decompression + Chrome launch + page render exceeded API Gateway's limit. And that's on a fast day. If you've seen these errors, you know the pain. We built Riddle so you don't have to deal with this anymore. The Serverless Browser Problem Every major serverless platform has limits that make headless Chrome difficult or impossible: AWS Lambda 250MB unzipped limit Chromium is 280MB. @sparticuz/chromium compresses to ~45MB but decompresses to /tmp on every cold start. VPC-enabled Lambdas can take 5+ minutes. Vercel Functions 50MB compressed limit Even aggressive compression doesn't work. Developers are stuck pinning ancient Puppeteer versions from 2021. Modern features are out of reach. Cloudflare Workers V8 isolates, no browser Workers run JavaScript, not containers. You literally cannot run Chrome. Cloudflare's Browser Rendering API exists but is rate-limited to 2/min. The @sparticuz/chromium Trap Over 300,000 developers download @sparticuz/chromium every week. Most of them are frustrated within hours. Version Pinning Hell Chromium version must match Puppeteer version must match Node.js runtime. AWS updates break your stack. You spend hours debugging compatibility. Cold Start Penalty Brotli decompression on every cold start. 5-10 seconds before your function even starts working. In VPCs, this can take minutes. Memory Instability Chrome loves RAM. Lambda doesn't have much. Your function crashes randomly on complex pages. \"spawn ENOMEM\" becomes your new nemesis. Runtime Update Roulette April 2024's Node.js 18.20 update froze Puppeteer for thousands of developers. You're one security patch away from a broken production system. As one developer put it: \"We had been burned by using third-party chromium packages and things could easily break with one security patch from AWS.\" The Solution: Don't Run Chrome in Lambda Instead of fighting Lambda limits, call an API. We run Playwright in proper infrastructure so you don't have to. 1 Your Lambda calls Riddle POST a URL or a Playwright script. We handle the browser. 2 We run Chrome properly No size limits, no memory constraints, no cold start decompression. Just Chrome running on real infrastructure. 3 You get your screenshot PNG bytes come back directly for simple captures. Use async polling or webhooks for longer workflows. Before and After Before: Lambda with @sparticuz/chromium (50+ lines of config) Copy // serverless.yml - just the chromium layer config layers: chromium: path: layer compatibleRuntimes: - nodejs18.x package: artifact: layer/chromium.zip // handler.js - and pray it works const chromium = require(\"@sparticuz/chromium\"); const puppeteer = require(\"puppeteer-core\"); exports.handler = async (event) => { // Configure for Lambda's weird environment chromium.setHeadlessMode = true; chromium.setGraphicsMode = false; const browser = await puppeteer.launch({ args: chromium.args, defaultViewport: chromium.defaultViewport, executablePath: await chromium.executablePath(), headless: chromium.headless, }); // Hope Chrome doesn't crash... const page = await browser.newPage(); await page.goto(event.url); const screenshot = await page.screenshot(); await browser.close(); return screenshot; }; After: One API call Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stringify({ url: \"https://example.com\" }) }); const png = await response.arrayBuffer(); // png contains your screenshot bytes Get Started Free Full Comparison Factor Self-Hosted Lambda Riddle API Setup time Hours of debugging layers, binaries, dependencies curl command in 2 minutes Size limit 250MB (Chromium is 280MB) Not your problem Memory needed 1-2GB minimum, crashes common Whatever your Lambda actually needs Cold start 5-10s (Brotli decompression) ~2s (HTTP call) Maintenance Breaks with Node.js runtime updates We handle it Version compatibility Chromium + Puppeteer + Node.js matrix Always latest Works on Vercel Barely (ancient versions only) Yes Works on Cloudflare No Yes Cost per job ~$0.001 + compute + your debugging time from $0.004 (all-in) Read the detailed breakdown → What You Can Do Simple Screenshots POST a URL, get a PNG. Pass auth headers to screenshot staging environments and protected pages. JSON Workflows Multi-page flows, form fills, clicks, waits. Send JSON steps, no code required. Batch Screenshots Take multiple screenshots in one job. Get below $0.001 per screenshot with batching. PDF Generation Render pages to PDF with full CSS support. No more wkhtmltopdf headaches. From $0.004 per job. Under $0.001 per screenshot when batched. See pricing → Stop Fighting Lambda Create an account and get screenshots in minutes, not hours. Get Started Free View Docs Riddle Distributed Computing Product Riddle Proof Good Catch Diary Pricing Maximize Value Documentation MCP Recipes Playground Use Cases AI Agents Agent Guide Serverless Authenticated Pages vs Self-Hosted Blog Evidence Over Summaries Frontend Dev Without a Browser E2E Testing Tips Chatty Agents (Part I) Batching Heuristics (Part II) Vision Agents Teaching AI to Ski Support FAQ Status Contact Legal Terms Privacy © 2026 Riddle. All rights reserved.",
        "scroll_width": 1280,
        "client_width": 1280,
        "overflow_px": 0,
        "bounds_overflow_px": 0,
        "overflow_offenders": [],
        "selectors": {
          ".landing-page": {
            "count": 1,
            "visible_count": 1
          },
          ".code-block-wrapper": {
            "count": 2,
            "visible_count": 2
          }
        },
        "frames": {},
        "text_sequences": {
          ".code-block-wrapper": {
            "count": 2,
            "visible_count": 2,
            "texts": [
              "Copy // serverless.yml - just the chromium layer config layers: chromium: path: layer compatibleRuntimes: - nodejs18.x package: artifact: layer/chromium.zip // handler.js - and pray it works const chromium = require(\"@sparticuz/chromium\"); ",
              "Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stri"
            ],
            "visible_texts": [
              "Copy // serverless.yml - just the chromium layer config layers: chromium: path: layer compatibleRuntimes: - nodejs18.x package: artifact: layer/chromium.zip // handler.js - and pray it works const chromium = require(\"@sparticuz/chromium\"); ",
              "Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stri"
            ],
            "match_texts": [
              "Copy // serverless.yml - just the chromium layer config layers: chromium: path: layer compatibleRuntimes: - nodejs18.x package: artifact: layer/chromium.zip // handler.js - and pray it works const chromium = require(\"@sparticuz/chromium\"); const puppeteer = require(\"puppeteer-core\"); exports.handler = async (event) => { // Configure for Lambda's weird environment chromium.setHeadlessMode = true; chromium.setGraphicsMode = false; const browser = await puppeteer.launch({ args: chromium.args, defaultViewport: chromium.defaultViewport, executablePath: await chromium.executablePath(), headless: chromium.headless, }); // Hope Chrome doesn't crash... const page = await browser.newPage(); await page.goto(event.url); const screenshot = await page.screenshot(); await browser.close(); return screenshot; };",
              "Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stringify({ url: \"https://example.com\" }) }); const png = await response.arrayBuffer(); // png contains your screenshot bytes"
            ],
            "visible_match_texts": [
              "Copy // serverless.yml - just the chromium layer config layers: chromium: path: layer compatibleRuntimes: - nodejs18.x package: artifact: layer/chromium.zip // handler.js - and pray it works const chromium = require(\"@sparticuz/chromium\"); const puppeteer = require(\"puppeteer-core\"); exports.handler = async (event) => { // Configure for Lambda's weird environment chromium.setHeadlessMode = true; chromium.setGraphicsMode = false; const browser = await puppeteer.launch({ args: chromium.args, defaultViewport: chromium.defaultViewport, executablePath: await chromium.executablePath(), headless: chromium.headless, }); // Hope Chrome doesn't crash... const page = await browser.newPage(); await page.goto(event.url); const screenshot = await page.screenshot(); await browser.close(); return screenshot; };",
              "Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stringify({ url: \"https://example.com\" }) }); const png = await response.arrayBuffer(); // png contains your screenshot bytes"
            ]
          },
          ".landing-page": {
            "count": 1,
            "visible_count": 1,
            "texts": [
              "Playwright for Serverless Chromium is ~280MB. Lambda's limit is 250MB. Vercel caps at 50MB. Cloudflare Workers can't run it at all. We solved all of these. Recognize These? Unzipped size must be smaller than 262144000 bytes Chromium is ~280"
            ],
            "visible_texts": [
              "Playwright for Serverless Chromium is ~280MB. Lambda's limit is 250MB. Vercel caps at 50MB. Cloudflare Workers can't run it at all. We solved all of these. Recognize These? Unzipped size must be smaller than 262144000 bytes Chromium is ~280"
            ],
            "match_texts": [
              "Playwright for Serverless Chromium is ~280MB. Lambda's limit is 250MB. Vercel caps at 50MB. Cloudflare Workers can't run it at all. We solved all of these. Recognize These? Unzipped size must be smaller than 262144000 bytes Chromium is ~280MB. Lambda's unzipped limit is 250MB. The math doesn't work, no matter how clever you get with compression. spawn ENOMEM Lambda ran out of memory trying to spawn Chrome. You need 512MB minimum, 1-2GB recommended. That gets expensive. error while loading shared libraries: libnss3.so Missing Linux dependencies on Amazon Linux. Time to debug Lambda layers and figure out which .so files are missing. Task timed out after 30 seconds Cold start + Brotli decompression + Chrome launch + page render exceeded API Gateway's limit. And that's on a fast day. If you've seen these errors, you know the pain. We built Riddle so you don't have to deal with this anymore. The Serverless Browser Problem Every major serverless platform has limits that make headless Chrome difficult or impossible: AWS Lambda 250MB unzipped limit Chromium is 280MB. @sparticuz/chromium compresses to ~45MB but decompresses to /tmp on every cold start. VPC-enabled Lambdas can take 5+ minutes. Vercel Functions 50MB compressed limit Even aggressive compression doesn't work. Developers are stuck pinning ancient Puppeteer versions from 2021. Modern features are out of reach. Cloudflare Workers V8 isolates, no browser Workers run JavaScript, not containers. You literally cannot run Chrome. Cloudflare's Browser Rendering API exists but is rate-limited to 2/min. The @sparticuz/chromium Trap Over 300,000 developers download @sparticuz/chromium every week. Most of them are frustrated within hours. Version Pinning Hell Chromium version must match Puppeteer version must match Node.js runtime. AWS updates break your stack. You spend hours debugging compatibility. Cold Start Penalty Brotli decompression on every cold start. 5-10 seconds before your function even starts working. In VPCs, this can take minutes. Memory Instability Chrome loves RAM. Lambda doesn't have much. Your function crashes randomly on complex pages. \"spawn ENOMEM\" becomes your new nemesis. Runtime Update Roulette April 2024's Node.js 18.20 update froze Puppeteer for thousands of developers. You're one security patch away from a broken production system. As one developer put it: \"We had been burned by using third-party chromium packages and things could easily break with one security patch from AWS.\" The Solution: Don't Run Chrome in Lambda Instead of fighting Lambda limits, call an API. We run Playwright in proper infrastructure so you don't have to. 1 Your Lambda calls Riddle POST a URL or a Playwright script. We handle the browser. 2 We run Chrome properly No size limits, no memory constraints, no cold start decompression. Just Chrome running on real infrastructure. 3 You get your screenshot PNG bytes come back directly for simple captures. Use async polling or webhooks for longer workflows. Before and After Before: Lambda with @sparticuz/chromium (50+ lines of config) Copy // serverless.yml - just the chromium layer config layers: chromium: path: layer compatibleRuntimes: - nodejs18.x package: artifact: layer/chromium.zip // handler.js - and pray it works const chromium = require(\"@sparticuz/chromium\"); const puppeteer = require(\"puppeteer-core\"); exports.handler = async (event) => { // Configure for Lambda's weird environment chromium.setHeadlessMode = true; chromium.setGraphicsMode = false; const browser = await puppeteer.launch({ args: chromium.args, defaultViewport: chromium.defaultViewport, executablePath: await chromium.executablePath(), headless: chromium.headless, }); // Hope Chrome doesn't crash... const page = await browser.newPage(); await page.goto(event.url); const screenshot = await page.screenshot(); await browser.close(); return screenshot; }; After: One API call Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stringify({ url: \"https://example.com\" }) }); const png = await response.arrayBuffer(); // png contains your screenshot bytes Get Started Free Full Comparison Factor Self-Hosted Lambda Riddle API Setup time Hours of debugging layers, binaries, dependencies curl command in 2 minutes Size limit 250MB (Chromium is 280MB) Not your problem Memory needed 1-2GB minimum, crashes common Whatever your Lambda actually needs Cold start 5-10s (Brotli decompression) ~2s (HTTP call) Maintenance Breaks with Node.js runtime updates We handle it Version compatibility Chromium + Puppeteer + Node.js matrix Always latest Works on Vercel Barely (ancient versions only) Yes Works on Cloudflare No Yes Cost per job ~$0.001 + compute + your debugging time from $0.004 (all-in) Read the detailed breakdown → What You Can Do Simple Screenshots POST a URL, get a PNG. Pass auth headers to screenshot staging environments and protected pages. JSON Workflows Multi-page flows, form fills, clicks, waits. Send JSON steps, no code required. Batch Screenshots Take multiple screenshots in one job. Get below $0.001 per screenshot with batching. PDF Generation Render pages to PDF with full CSS support. No more wkhtmltopdf headaches. From $0.004 per job. Under $0.001 per screenshot when batched. See pricing → Stop Fighting Lambda Create an account and get screenshots in minutes, not hours. Get Started Free View Docs"
            ],
            "visible_match_texts": [
              "Playwright for Serverless Chromium is ~280MB. Lambda's limit is 250MB. Vercel caps at 50MB. Cloudflare Workers can't run it at all. We solved all of these. Recognize These? Unzipped size must be smaller than 262144000 bytes Chromium is ~280MB. Lambda's unzipped limit is 250MB. The math doesn't work, no matter how clever you get with compression. spawn ENOMEM Lambda ran out of memory trying to spawn Chrome. You need 512MB minimum, 1-2GB recommended. That gets expensive. error while loading shared libraries: libnss3.so Missing Linux dependencies on Amazon Linux. Time to debug Lambda layers and figure out which .so files are missing. Task timed out after 30 seconds Cold start + Brotli decompression + Chrome launch + page render exceeded API Gateway's limit. And that's on a fast day. If you've seen these errors, you know the pain. We built Riddle so you don't have to deal with this anymore. The Serverless Browser Problem Every major serverless platform has limits that make headless Chrome difficult or impossible: AWS Lambda 250MB unzipped limit Chromium is 280MB. @sparticuz/chromium compresses to ~45MB but decompresses to /tmp on every cold start. VPC-enabled Lambdas can take 5+ minutes. Vercel Functions 50MB compressed limit Even aggressive compression doesn't work. Developers are stuck pinning ancient Puppeteer versions from 2021. Modern features are out of reach. Cloudflare Workers V8 isolates, no browser Workers run JavaScript, not containers. You literally cannot run Chrome. Cloudflare's Browser Rendering API exists but is rate-limited to 2/min. The @sparticuz/chromium Trap Over 300,000 developers download @sparticuz/chromium every week. Most of them are frustrated within hours. Version Pinning Hell Chromium version must match Puppeteer version must match Node.js runtime. AWS updates break your stack. You spend hours debugging compatibility. Cold Start Penalty Brotli decompression on every cold start. 5-10 seconds before your function even starts working. In VPCs, this can take minutes. Memory Instability Chrome loves RAM. Lambda doesn't have much. Your function crashes randomly on complex pages. \"spawn ENOMEM\" becomes your new nemesis. Runtime Update Roulette April 2024's Node.js 18.20 update froze Puppeteer for thousands of developers. You're one security patch away from a broken production system. As one developer put it: \"We had been burned by using third-party chromium packages and things could easily break with one security patch from AWS.\" The Solution: Don't Run Chrome in Lambda Instead of fighting Lambda limits, call an API. We run Playwright in proper infrastructure so you don't have to. 1 Your Lambda calls Riddle POST a URL or a Playwright script. We handle the browser. 2 We run Chrome properly No size limits, no memory constraints, no cold start decompression. Just Chrome running on real infrastructure. 3 You get your screenshot PNG bytes come back directly for simple captures. Use async polling or webhooks for longer workflows. Before and After Before: Lambda with @sparticuz/chromium (50+ lines of config) Copy // serverless.yml - just the chromium layer config layers: chromium: path: layer compatibleRuntimes: - nodejs18.x package: artifact: layer/chromium.zip // handler.js - and pray it works const chromium = require(\"@sparticuz/chromium\"); const puppeteer = require(\"puppeteer-core\"); exports.handler = async (event) => { // Configure for Lambda's weird environment chromium.setHeadlessMode = true; chromium.setGraphicsMode = false; const browser = await puppeteer.launch({ args: chromium.args, defaultViewport: chromium.defaultViewport, executablePath: await chromium.executablePath(), headless: chromium.headless, }); // Hope Chrome doesn't crash... const page = await browser.newPage(); await page.goto(event.url); const screenshot = await page.screenshot(); await browser.close(); return screenshot; }; After: One API call Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stringify({ url: \"https://example.com\" }) }); const png = await response.arrayBuffer(); // png contains your screenshot bytes Get Started Free Full Comparison Factor Self-Hosted Lambda Riddle API Setup time Hours of debugging layers, binaries, dependencies curl command in 2 minutes Size limit 250MB (Chromium is 280MB) Not your problem Memory needed 1-2GB minimum, crashes common Whatever your Lambda actually needs Cold start 5-10s (Brotli decompression) ~2s (HTTP call) Maintenance Breaks with Node.js runtime updates We handle it Version compatibility Chromium + Puppeteer + Node.js matrix Always latest Works on Vercel Barely (ancient versions only) Yes Works on Cloudflare No Yes Cost per job ~$0.001 + compute + your debugging time from $0.004 (all-in) Read the detailed breakdown → What You Can Do Simple Screenshots POST a URL, get a PNG. Pass auth headers to screenshot staging environments and protected pages. JSON Workflows Multi-page flows, form fills, clicks, waits. Send JSON steps, no code required. Batch Screenshots Take multiple screenshots in one job. Get below $0.001 per screenshot with batching. PDF Generation Render pages to PDF with full CSS support. No more wkhtmltopdf headaches. From $0.004 per job. Under $0.001 per screenshot when batched. See pricing → Stop Fighting Lambda Create an account and get screenshots in minutes, not hours. Get Started Free View Docs"
            ]
          }
        },
        "text_matches": {
          "text:Playwright for Serverless": true,
          "text:After: One API call": true,
          "text:POST a URL, get a PNG": true
        },
        "http_statuses": {},
        "link_statuses": {
          "a[href*='/register/'], a[href*='/docs/'], a[href*='/pricing/'], a[href*='/vs-self-hosted/']": {
            "version": "riddle-proof.link-status.v1",
            "selector": "a[href*='/register/'], a[href*='/docs/'], a[href*='/pricing/'], a[href*='/vs-self-hosted/']",
            "max_links": 20,
            "same_origin_only": true,
            "dedupe": true,
            "require_nonzero_bytes": true,
            "min_bytes": 1000,
            "allowed_content_types": [
              "text/html"
            ],
            "allowed_statuses": [
              "2xx",
              "3xx"
            ],
            "discovered_count": 12,
            "total_count": 4,
            "truncated": false,
            "ok_count": 4,
            "failed_count": 0,
            "status_counts": {
              "206": 4
            },
            "failures": [],
            "result_count": 4,
            "stored_result_count": 4,
            "omitted_result_count": 0,
            "omitted_success_count": 0,
            "results_compacted": false,
            "results": [
              {
                "url": "https://riddledc.com/docs/",
                "tag": "a",
                "text": "Docs",
                "status": 206,
                "method": "GET",
                "ok": true,
                "content_type": "text/html; charset=utf-8",
                "content_length": 1000,
                "bytes": 1000,
                "redirected": false,
                "final_url": "https://riddledc.com/docs/",
                "error": null
              },
              {
                "url": "https://riddledc.com/pricing/",
                "tag": "a",
                "text": "Pricing",
                "status": 206,
                "method": "GET",
                "ok": true,
                "content_type": "text/html; charset=utf-8",
                "content_length": 1000,
                "bytes": 1000,
                "redirected": false,
                "final_url": "https://riddledc.com/pricing/",
                "error": null
              },
              {
                "url": "https://riddledc.com/register/",
                "tag": "a",
                "text": "Sign Up",
                "status": 206,
                "method": "GET",
                "ok": true,
                "content_type": "text/html; charset=utf-8",
                "content_length": 1000,
                "bytes": 1000,
                "redirected": false,
                "final_url": "https://riddledc.com/register/",
                "error": null
              },
              {
                "url": "https://riddledc.com/vs-self-hosted/",
                "tag": "a",
                "text": "Read the detailed breakdown →",
                "status": 206,
                "method": "GET",
                "ok": true,
                "content_type": "text/html; charset=utf-8",
                "content_length": 1000,
                "bytes": 1000,
                "redirected": false,
                "final_url": "https://riddledc.com/vs-self-hosted/",
                "error": null
              }
            ]
          }
        },
        "setup_action_results": [],
        "screenshot_label": "riddle-site-v491-serverless-simple-screenshot-response-desktop"
      },
      {
        "name": "phone",
        "width": 390,
        "height": 844,
        "url": "https://riddledc.com/serverless/",
        "route": {
          "requested": "https://riddledc.com/serverless/",
          "observed": "/serverless/",
          "expected_path": "/serverless/",
          "matched": true,
          "http_status": 200
        },
        "title": "Serverless Browser Automation - Riddle",
        "body_text_length": 6011,
        "body_text_sample": "Skip to main content Riddle Playwright for Serverless Chromium is ~280MB. Lambda's limit is 250MB. Vercel caps at 50MB. Cloudflare Workers can't run it at all. We solved all of these. Recognize These? Unzipped size must be smaller than 262144000 bytes Chromium is ~280MB. Lambda's unzipped limit is 250MB. The math doesn't work, no matter how clever you get with compression. spawn ENOMEM Lambda ran out of memory trying to spawn Chrome. You need 512MB minimum, 1-2GB recommended. That gets expensive. error while loading shared libraries: libnss3.so Missing Linux dependencies on Amazon Linux. Time to debug Lambda layers and figure out which .so files are missing. Task timed out after 30 seconds Cold start + Brotli decompression + Chrome launch + page render exceeded API Gateway's limit. And that's on a fast day. If you've seen these errors, you know the pain. We built Riddle so you don't have to deal with this anymore. The Serverless Browser Problem Every major serverless platform has limits that make headless Chrome difficult or impossible: AWS Lambda 250MB unzipped limit Chromium is 280MB. @sparticuz/chromium compresses to ~45MB but decompresses to /tmp on every cold start. VPC-enabled Lambdas can take 5+ minutes. Vercel Functions 50MB compressed limit Even aggressive compression doesn't work. Developers are stuck pinning ancient Puppeteer versions from 2021. Modern features are out of reach. Cloudflare Workers V8 isolates, no browser Workers run JavaScript, not containers. You literally cannot run Chrome. Cloudflare's Browser Rendering API exists but is rate-limited to 2/min. The @sparticuz/chromium Trap Over 300,000 developers download @sparticuz/chromium every week. Most of them are frustrated within hours. Version Pinning Hell Chromium version must match Puppeteer version must match Node.js runtime. AWS updates break your stack. You spend hours debugging compatibility. Cold Start Penalty Brotli decompression on every cold start. 5-10 seconds before your function even starts working. In VPCs, this can take minutes. Memory Instability Chrome loves RAM. Lambda doesn't have much. Your function crashes randomly on complex pages. \"spawn ENOMEM\" becomes your new nemesis. Runtime Update Roulette April 2024's Node.js 18.20 update froze Puppeteer for thousands of developers. You're one security patch away from a broken production system. As one developer put it: \"We had been burned by using third-party chromium packages and things could easily break with one security patch from AWS.\" The Solution: Don't Run Chrome in Lambda Instead of fighting Lambda limits, call an API. We run Playwright in proper infrastructure so you don't have to. 1 Your Lambda calls Riddle POST a URL or a Playwright script. We handle the browser. 2 We run Chrome properly No size limits, no memory constraints, no cold start decompression. Just Chrome running on real infrastructure. 3 You get your screenshot PNG bytes come back directly for simple captures. Use async polling or webhooks for longer workflows. Before and After Before: Lambda with @sparticuz/chromium (50+ lines of config) Copy // serverless.yml - just the chromium layer config layers: chromium: path: layer compatibleRuntimes: - nodejs18.x package: artifact: layer/chromium.zip // handler.js - and pray it works const chromium = require(\"@sparticuz/chromium\"); const puppeteer = require(\"puppeteer-core\"); exports.handler = async (event) => { // Configure for Lambda's weird environment chromium.setHeadlessMode = true; chromium.setGraphicsMode = false; const browser = await puppeteer.launch({ args: chromium.args, defaultViewport: chromium.defaultViewport, executablePath: await chromium.executablePath(), headless: chromium.headless, }); // Hope Chrome doesn't crash... const page = await browser.newPage(); await page.goto(event.url); const screenshot = await page.screenshot(); await browser.close(); return screenshot; }; After: One API call Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stringify({ url: \"https://example.com\" }) }); const png = await response.arrayBuffer(); // png contains your screenshot bytes Get Started Free Full Comparison Factor Self-Hosted Lambda Riddle API Setup time Hours of debugging layers, binaries, dependencies curl command in 2 minutes Size limit 250MB (Chromium is 280MB) Not your problem Memory needed 1-2GB minimum, crashes common Whatever your Lambda actually needs Cold start 5-10s (Brotli decompression) ~2s (HTTP call) Maintenance Breaks with Node.js runtime updates We handle it Version compatibility Chromium + Puppeteer + Node.js matrix Always latest Works on Vercel Barely (ancient versions only) Yes Works on Cloudflare No Yes Cost per job ~$0.001 + compute + your debugging time from $0.004 (all-in) Read the detailed breakdown → What You Can Do Simple Screenshots POST a URL, get a PNG. Pass auth headers to screenshot staging environments and protected pages. JSON Workflows Multi-page flows, form fills, clicks, waits. Send JSON steps, no code required. Batch Screenshots Take multiple screenshots in one job. Get below $0.001 per screenshot with batching. PDF Generation Render pages to PDF with full CSS support. No more wkhtmltopdf headaches. From $0.004 per job. Under $0.001 per screenshot when batched. See pricing → Stop Fighting Lambda Create an account and get screenshots in minutes, not hours. Get Started Free View Docs Riddle Distributed Computing Product Riddle Proof Good Catch Diary Pricing Maximize Value Documentation MCP Recipes Playground Use Cases AI Agents Agent Guide Serverless Authenticated Pages vs Self-Hosted Blog Evidence Over Summaries Frontend Dev Without a Browser E2E Testing Tips Chatty Agents (Part I) Batching Heuristics (Part II) Vision Agents Teaching AI to Ski Support FAQ Status Contact Legal Terms Privacy © 2026 Riddle. All rights reserved.",
        "scroll_width": 390,
        "client_width": 390,
        "overflow_px": 0,
        "bounds_overflow_px": 0,
        "overflow_offenders": [],
        "selectors": {
          ".landing-page": {
            "count": 1,
            "visible_count": 1
          },
          ".code-block-wrapper": {
            "count": 2,
            "visible_count": 2
          }
        },
        "frames": {},
        "text_sequences": {
          ".code-block-wrapper": {
            "count": 2,
            "visible_count": 2,
            "texts": [
              "Copy // serverless.yml - just the chromium layer config layers: chromium: path: layer compatibleRuntimes: - nodejs18.x package: artifact: layer/chromium.zip // handler.js - and pray it works const chromium = require(\"@sparticuz/chromium\"); ",
              "Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stri"
            ],
            "visible_texts": [
              "Copy // serverless.yml - just the chromium layer config layers: chromium: path: layer compatibleRuntimes: - nodejs18.x package: artifact: layer/chromium.zip // handler.js - and pray it works const chromium = require(\"@sparticuz/chromium\"); ",
              "Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stri"
            ],
            "match_texts": [
              "Copy // serverless.yml - just the chromium layer config layers: chromium: path: layer compatibleRuntimes: - nodejs18.x package: artifact: layer/chromium.zip // handler.js - and pray it works const chromium = require(\"@sparticuz/chromium\"); const puppeteer = require(\"puppeteer-core\"); exports.handler = async (event) => { // Configure for Lambda's weird environment chromium.setHeadlessMode = true; chromium.setGraphicsMode = false; const browser = await puppeteer.launch({ args: chromium.args, defaultViewport: chromium.defaultViewport, executablePath: await chromium.executablePath(), headless: chromium.headless, }); // Hope Chrome doesn't crash... const page = await browser.newPage(); await page.goto(event.url); const screenshot = await page.screenshot(); await browser.close(); return screenshot; };",
              "Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stringify({ url: \"https://example.com\" }) }); const png = await response.arrayBuffer(); // png contains your screenshot bytes"
            ],
            "visible_match_texts": [
              "Copy // serverless.yml - just the chromium layer config layers: chromium: path: layer compatibleRuntimes: - nodejs18.x package: artifact: layer/chromium.zip // handler.js - and pray it works const chromium = require(\"@sparticuz/chromium\"); const puppeteer = require(\"puppeteer-core\"); exports.handler = async (event) => { // Configure for Lambda's weird environment chromium.setHeadlessMode = true; chromium.setGraphicsMode = false; const browser = await puppeteer.launch({ args: chromium.args, defaultViewport: chromium.defaultViewport, executablePath: await chromium.executablePath(), headless: chromium.headless, }); // Hope Chrome doesn't crash... const page = await browser.newPage(); await page.goto(event.url); const screenshot = await page.screenshot(); await browser.close(); return screenshot; };",
              "Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stringify({ url: \"https://example.com\" }) }); const png = await response.arrayBuffer(); // png contains your screenshot bytes"
            ]
          },
          ".landing-page": {
            "count": 1,
            "visible_count": 1,
            "texts": [
              "Playwright for Serverless Chromium is ~280MB. Lambda's limit is 250MB. Vercel caps at 50MB. Cloudflare Workers can't run it at all. We solved all of these. Recognize These? Unzipped size must be smaller than 262144000 bytes Chromium is ~280"
            ],
            "visible_texts": [
              "Playwright for Serverless Chromium is ~280MB. Lambda's limit is 250MB. Vercel caps at 50MB. Cloudflare Workers can't run it at all. We solved all of these. Recognize These? Unzipped size must be smaller than 262144000 bytes Chromium is ~280"
            ],
            "match_texts": [
              "Playwright for Serverless Chromium is ~280MB. Lambda's limit is 250MB. Vercel caps at 50MB. Cloudflare Workers can't run it at all. We solved all of these. Recognize These? Unzipped size must be smaller than 262144000 bytes Chromium is ~280MB. Lambda's unzipped limit is 250MB. The math doesn't work, no matter how clever you get with compression. spawn ENOMEM Lambda ran out of memory trying to spawn Chrome. You need 512MB minimum, 1-2GB recommended. That gets expensive. error while loading shared libraries: libnss3.so Missing Linux dependencies on Amazon Linux. Time to debug Lambda layers and figure out which .so files are missing. Task timed out after 30 seconds Cold start + Brotli decompression + Chrome launch + page render exceeded API Gateway's limit. And that's on a fast day. If you've seen these errors, you know the pain. We built Riddle so you don't have to deal with this anymore. The Serverless Browser Problem Every major serverless platform has limits that make headless Chrome difficult or impossible: AWS Lambda 250MB unzipped limit Chromium is 280MB. @sparticuz/chromium compresses to ~45MB but decompresses to /tmp on every cold start. VPC-enabled Lambdas can take 5+ minutes. Vercel Functions 50MB compressed limit Even aggressive compression doesn't work. Developers are stuck pinning ancient Puppeteer versions from 2021. Modern features are out of reach. Cloudflare Workers V8 isolates, no browser Workers run JavaScript, not containers. You literally cannot run Chrome. Cloudflare's Browser Rendering API exists but is rate-limited to 2/min. The @sparticuz/chromium Trap Over 300,000 developers download @sparticuz/chromium every week. Most of them are frustrated within hours. Version Pinning Hell Chromium version must match Puppeteer version must match Node.js runtime. AWS updates break your stack. You spend hours debugging compatibility. Cold Start Penalty Brotli decompression on every cold start. 5-10 seconds before your function even starts working. In VPCs, this can take minutes. Memory Instability Chrome loves RAM. Lambda doesn't have much. Your function crashes randomly on complex pages. \"spawn ENOMEM\" becomes your new nemesis. Runtime Update Roulette April 2024's Node.js 18.20 update froze Puppeteer for thousands of developers. You're one security patch away from a broken production system. As one developer put it: \"We had been burned by using third-party chromium packages and things could easily break with one security patch from AWS.\" The Solution: Don't Run Chrome in Lambda Instead of fighting Lambda limits, call an API. We run Playwright in proper infrastructure so you don't have to. 1 Your Lambda calls Riddle POST a URL or a Playwright script. We handle the browser. 2 We run Chrome properly No size limits, no memory constraints, no cold start decompression. Just Chrome running on real infrastructure. 3 You get your screenshot PNG bytes come back directly for simple captures. Use async polling or webhooks for longer workflows. Before and After Before: Lambda with @sparticuz/chromium (50+ lines of config) Copy // serverless.yml - just the chromium layer config layers: chromium: path: layer compatibleRuntimes: - nodejs18.x package: artifact: layer/chromium.zip // handler.js - and pray it works const chromium = require(\"@sparticuz/chromium\"); const puppeteer = require(\"puppeteer-core\"); exports.handler = async (event) => { // Configure for Lambda's weird environment chromium.setHeadlessMode = true; chromium.setGraphicsMode = false; const browser = await puppeteer.launch({ args: chromium.args, defaultViewport: chromium.defaultViewport, executablePath: await chromium.executablePath(), headless: chromium.headless, }); // Hope Chrome doesn't crash... const page = await browser.newPage(); await page.goto(event.url); const screenshot = await page.screenshot(); await browser.close(); return screenshot; }; After: One API call Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stringify({ url: \"https://example.com\" }) }); const png = await response.arrayBuffer(); // png contains your screenshot bytes Get Started Free Full Comparison Factor Self-Hosted Lambda Riddle API Setup time Hours of debugging layers, binaries, dependencies curl command in 2 minutes Size limit 250MB (Chromium is 280MB) Not your problem Memory needed 1-2GB minimum, crashes common Whatever your Lambda actually needs Cold start 5-10s (Brotli decompression) ~2s (HTTP call) Maintenance Breaks with Node.js runtime updates We handle it Version compatibility Chromium + Puppeteer + Node.js matrix Always latest Works on Vercel Barely (ancient versions only) Yes Works on Cloudflare No Yes Cost per job ~$0.001 + compute + your debugging time from $0.004 (all-in) Read the detailed breakdown → What You Can Do Simple Screenshots POST a URL, get a PNG. Pass auth headers to screenshot staging environments and protected pages. JSON Workflows Multi-page flows, form fills, clicks, waits. Send JSON steps, no code required. Batch Screenshots Take multiple screenshots in one job. Get below $0.001 per screenshot with batching. PDF Generation Render pages to PDF with full CSS support. No more wkhtmltopdf headaches. From $0.004 per job. Under $0.001 per screenshot when batched. See pricing → Stop Fighting Lambda Create an account and get screenshots in minutes, not hours. Get Started Free View Docs"
            ],
            "visible_match_texts": [
              "Playwright for Serverless Chromium is ~280MB. Lambda's limit is 250MB. Vercel caps at 50MB. Cloudflare Workers can't run it at all. We solved all of these. Recognize These? Unzipped size must be smaller than 262144000 bytes Chromium is ~280MB. Lambda's unzipped limit is 250MB. The math doesn't work, no matter how clever you get with compression. spawn ENOMEM Lambda ran out of memory trying to spawn Chrome. You need 512MB minimum, 1-2GB recommended. That gets expensive. error while loading shared libraries: libnss3.so Missing Linux dependencies on Amazon Linux. Time to debug Lambda layers and figure out which .so files are missing. Task timed out after 30 seconds Cold start + Brotli decompression + Chrome launch + page render exceeded API Gateway's limit. And that's on a fast day. If you've seen these errors, you know the pain. We built Riddle so you don't have to deal with this anymore. The Serverless Browser Problem Every major serverless platform has limits that make headless Chrome difficult or impossible: AWS Lambda 250MB unzipped limit Chromium is 280MB. @sparticuz/chromium compresses to ~45MB but decompresses to /tmp on every cold start. VPC-enabled Lambdas can take 5+ minutes. Vercel Functions 50MB compressed limit Even aggressive compression doesn't work. Developers are stuck pinning ancient Puppeteer versions from 2021. Modern features are out of reach. Cloudflare Workers V8 isolates, no browser Workers run JavaScript, not containers. You literally cannot run Chrome. Cloudflare's Browser Rendering API exists but is rate-limited to 2/min. The @sparticuz/chromium Trap Over 300,000 developers download @sparticuz/chromium every week. Most of them are frustrated within hours. Version Pinning Hell Chromium version must match Puppeteer version must match Node.js runtime. AWS updates break your stack. You spend hours debugging compatibility. Cold Start Penalty Brotli decompression on every cold start. 5-10 seconds before your function even starts working. In VPCs, this can take minutes. Memory Instability Chrome loves RAM. Lambda doesn't have much. Your function crashes randomly on complex pages. \"spawn ENOMEM\" becomes your new nemesis. Runtime Update Roulette April 2024's Node.js 18.20 update froze Puppeteer for thousands of developers. You're one security patch away from a broken production system. As one developer put it: \"We had been burned by using third-party chromium packages and things could easily break with one security patch from AWS.\" The Solution: Don't Run Chrome in Lambda Instead of fighting Lambda limits, call an API. We run Playwright in proper infrastructure so you don't have to. 1 Your Lambda calls Riddle POST a URL or a Playwright script. We handle the browser. 2 We run Chrome properly No size limits, no memory constraints, no cold start decompression. Just Chrome running on real infrastructure. 3 You get your screenshot PNG bytes come back directly for simple captures. Use async polling or webhooks for longer workflows. Before and After Before: Lambda with @sparticuz/chromium (50+ lines of config) Copy // serverless.yml - just the chromium layer config layers: chromium: path: layer compatibleRuntimes: - nodejs18.x package: artifact: layer/chromium.zip // handler.js - and pray it works const chromium = require(\"@sparticuz/chromium\"); const puppeteer = require(\"puppeteer-core\"); exports.handler = async (event) => { // Configure for Lambda's weird environment chromium.setHeadlessMode = true; chromium.setGraphicsMode = false; const browser = await puppeteer.launch({ args: chromium.args, defaultViewport: chromium.defaultViewport, executablePath: await chromium.executablePath(), headless: chromium.headless, }); // Hope Chrome doesn't crash... const page = await browser.newPage(); await page.goto(event.url); const screenshot = await page.screenshot(); await browser.close(); return screenshot; }; After: One API call Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stringify({ url: \"https://example.com\" }) }); const png = await response.arrayBuffer(); // png contains your screenshot bytes Get Started Free Full Comparison Factor Self-Hosted Lambda Riddle API Setup time Hours of debugging layers, binaries, dependencies curl command in 2 minutes Size limit 250MB (Chromium is 280MB) Not your problem Memory needed 1-2GB minimum, crashes common Whatever your Lambda actually needs Cold start 5-10s (Brotli decompression) ~2s (HTTP call) Maintenance Breaks with Node.js runtime updates We handle it Version compatibility Chromium + Puppeteer + Node.js matrix Always latest Works on Vercel Barely (ancient versions only) Yes Works on Cloudflare No Yes Cost per job ~$0.001 + compute + your debugging time from $0.004 (all-in) Read the detailed breakdown → What You Can Do Simple Screenshots POST a URL, get a PNG. Pass auth headers to screenshot staging environments and protected pages. JSON Workflows Multi-page flows, form fills, clicks, waits. Send JSON steps, no code required. Batch Screenshots Take multiple screenshots in one job. Get below $0.001 per screenshot with batching. PDF Generation Render pages to PDF with full CSS support. No more wkhtmltopdf headaches. From $0.004 per job. Under $0.001 per screenshot when batched. See pricing → Stop Fighting Lambda Create an account and get screenshots in minutes, not hours. Get Started Free View Docs"
            ]
          }
        },
        "text_matches": {
          "text:Playwright for Serverless": true,
          "text:After: One API call": true,
          "text:POST a URL, get a PNG": true
        },
        "http_statuses": {},
        "link_statuses": {
          "a[href*='/register/'], a[href*='/docs/'], a[href*='/pricing/'], a[href*='/vs-self-hosted/']": {
            "version": "riddle-proof.link-status.v1",
            "selector": "a[href*='/register/'], a[href*='/docs/'], a[href*='/pricing/'], a[href*='/vs-self-hosted/']",
            "max_links": 20,
            "same_origin_only": true,
            "dedupe": true,
            "require_nonzero_bytes": true,
            "min_bytes": 1000,
            "allowed_content_types": [
              "text/html"
            ],
            "allowed_statuses": [
              "2xx",
              "3xx"
            ],
            "discovered_count": 12,
            "total_count": 4,
            "truncated": false,
            "ok_count": 4,
            "failed_count": 0,
            "status_counts": {
              "206": 4
            },
            "failures": [],
            "result_count": 4,
            "stored_result_count": 4,
            "omitted_result_count": 0,
            "omitted_success_count": 0,
            "results_compacted": false,
            "results": [
              {
                "url": "https://riddledc.com/docs/",
                "tag": "a",
                "text": "Docs",
                "status": 206,
                "method": "GET",
                "ok": true,
                "content_type": "text/html; charset=utf-8",
                "content_length": 1000,
                "bytes": 1000,
                "redirected": false,
                "final_url": "https://riddledc.com/docs/",
                "error": null
              },
              {
                "url": "https://riddledc.com/pricing/",
                "tag": "a",
                "text": "Pricing",
                "status": 206,
                "method": "GET",
                "ok": true,
                "content_type": "text/html; charset=utf-8",
                "content_length": 1000,
                "bytes": 1000,
                "redirected": false,
                "final_url": "https://riddledc.com/pricing/",
                "error": null
              },
              {
                "url": "https://riddledc.com/register/",
                "tag": "a",
                "text": "Sign Up",
                "status": 206,
                "method": "GET",
                "ok": true,
                "content_type": "text/html; charset=utf-8",
                "content_length": 1000,
                "bytes": 1000,
                "redirected": false,
                "final_url": "https://riddledc.com/register/",
                "error": null
              },
              {
                "url": "https://riddledc.com/vs-self-hosted/",
                "tag": "a",
                "text": "Read the detailed breakdown →",
                "status": 206,
                "method": "GET",
                "ok": true,
                "content_type": "text/html; charset=utf-8",
                "content_length": 1000,
                "bytes": 1000,
                "redirected": false,
                "final_url": "https://riddledc.com/vs-self-hosted/",
                "error": null
              }
            ]
          }
        },
        "setup_action_results": [],
        "screenshot_label": "riddle-site-v491-serverless-simple-screenshot-response-phone"
      },
      {
        "name": "ipad-mini",
        "width": 768,
        "height": 1024,
        "url": "https://riddledc.com/serverless/",
        "route": {
          "requested": "https://riddledc.com/serverless/",
          "observed": "/serverless/",
          "expected_path": "/serverless/",
          "matched": true,
          "http_status": 200
        },
        "title": "Serverless Browser Automation - Riddle",
        "body_text_length": 6011,
        "body_text_sample": "Skip to main content Riddle Playwright for Serverless Chromium is ~280MB. Lambda's limit is 250MB. Vercel caps at 50MB. Cloudflare Workers can't run it at all. We solved all of these. Recognize These? Unzipped size must be smaller than 262144000 bytes Chromium is ~280MB. Lambda's unzipped limit is 250MB. The math doesn't work, no matter how clever you get with compression. spawn ENOMEM Lambda ran out of memory trying to spawn Chrome. You need 512MB minimum, 1-2GB recommended. That gets expensive. error while loading shared libraries: libnss3.so Missing Linux dependencies on Amazon Linux. Time to debug Lambda layers and figure out which .so files are missing. Task timed out after 30 seconds Cold start + Brotli decompression + Chrome launch + page render exceeded API Gateway's limit. And that's on a fast day. If you've seen these errors, you know the pain. We built Riddle so you don't have to deal with this anymore. The Serverless Browser Problem Every major serverless platform has limits that make headless Chrome difficult or impossible: AWS Lambda 250MB unzipped limit Chromium is 280MB. @sparticuz/chromium compresses to ~45MB but decompresses to /tmp on every cold start. VPC-enabled Lambdas can take 5+ minutes. Vercel Functions 50MB compressed limit Even aggressive compression doesn't work. Developers are stuck pinning ancient Puppeteer versions from 2021. Modern features are out of reach. Cloudflare Workers V8 isolates, no browser Workers run JavaScript, not containers. You literally cannot run Chrome. Cloudflare's Browser Rendering API exists but is rate-limited to 2/min. The @sparticuz/chromium Trap Over 300,000 developers download @sparticuz/chromium every week. Most of them are frustrated within hours. Version Pinning Hell Chromium version must match Puppeteer version must match Node.js runtime. AWS updates break your stack. You spend hours debugging compatibility. Cold Start Penalty Brotli decompression on every cold start. 5-10 seconds before your function even starts working. In VPCs, this can take minutes. Memory Instability Chrome loves RAM. Lambda doesn't have much. Your function crashes randomly on complex pages. \"spawn ENOMEM\" becomes your new nemesis. Runtime Update Roulette April 2024's Node.js 18.20 update froze Puppeteer for thousands of developers. You're one security patch away from a broken production system. As one developer put it: \"We had been burned by using third-party chromium packages and things could easily break with one security patch from AWS.\" The Solution: Don't Run Chrome in Lambda Instead of fighting Lambda limits, call an API. We run Playwright in proper infrastructure so you don't have to. 1 Your Lambda calls Riddle POST a URL or a Playwright script. We handle the browser. 2 We run Chrome properly No size limits, no memory constraints, no cold start decompression. Just Chrome running on real infrastructure. 3 You get your screenshot PNG bytes come back directly for simple captures. Use async polling or webhooks for longer workflows. Before and After Before: Lambda with @sparticuz/chromium (50+ lines of config) Copy // serverless.yml - just the chromium layer config layers: chromium: path: layer compatibleRuntimes: - nodejs18.x package: artifact: layer/chromium.zip // handler.js - and pray it works const chromium = require(\"@sparticuz/chromium\"); const puppeteer = require(\"puppeteer-core\"); exports.handler = async (event) => { // Configure for Lambda's weird environment chromium.setHeadlessMode = true; chromium.setGraphicsMode = false; const browser = await puppeteer.launch({ args: chromium.args, defaultViewport: chromium.defaultViewport, executablePath: await chromium.executablePath(), headless: chromium.headless, }); // Hope Chrome doesn't crash... const page = await browser.newPage(); await page.goto(event.url); const screenshot = await page.screenshot(); await browser.close(); return screenshot; }; After: One API call Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stringify({ url: \"https://example.com\" }) }); const png = await response.arrayBuffer(); // png contains your screenshot bytes Get Started Free Full Comparison Factor Self-Hosted Lambda Riddle API Setup time Hours of debugging layers, binaries, dependencies curl command in 2 minutes Size limit 250MB (Chromium is 280MB) Not your problem Memory needed 1-2GB minimum, crashes common Whatever your Lambda actually needs Cold start 5-10s (Brotli decompression) ~2s (HTTP call) Maintenance Breaks with Node.js runtime updates We handle it Version compatibility Chromium + Puppeteer + Node.js matrix Always latest Works on Vercel Barely (ancient versions only) Yes Works on Cloudflare No Yes Cost per job ~$0.001 + compute + your debugging time from $0.004 (all-in) Read the detailed breakdown → What You Can Do Simple Screenshots POST a URL, get a PNG. Pass auth headers to screenshot staging environments and protected pages. JSON Workflows Multi-page flows, form fills, clicks, waits. Send JSON steps, no code required. Batch Screenshots Take multiple screenshots in one job. Get below $0.001 per screenshot with batching. PDF Generation Render pages to PDF with full CSS support. No more wkhtmltopdf headaches. From $0.004 per job. Under $0.001 per screenshot when batched. See pricing → Stop Fighting Lambda Create an account and get screenshots in minutes, not hours. Get Started Free View Docs Riddle Distributed Computing Product Riddle Proof Good Catch Diary Pricing Maximize Value Documentation MCP Recipes Playground Use Cases AI Agents Agent Guide Serverless Authenticated Pages vs Self-Hosted Blog Evidence Over Summaries Frontend Dev Without a Browser E2E Testing Tips Chatty Agents (Part I) Batching Heuristics (Part II) Vision Agents Teaching AI to Ski Support FAQ Status Contact Legal Terms Privacy © 2026 Riddle. All rights reserved.",
        "scroll_width": 768,
        "client_width": 768,
        "overflow_px": 0,
        "bounds_overflow_px": 0,
        "overflow_offenders": [],
        "selectors": {
          ".landing-page": {
            "count": 1,
            "visible_count": 1
          },
          ".code-block-wrapper": {
            "count": 2,
            "visible_count": 2
          }
        },
        "frames": {},
        "text_sequences": {
          ".code-block-wrapper": {
            "count": 2,
            "visible_count": 2,
            "texts": [
              "Copy // serverless.yml - just the chromium layer config layers: chromium: path: layer compatibleRuntimes: - nodejs18.x package: artifact: layer/chromium.zip // handler.js - and pray it works const chromium = require(\"@sparticuz/chromium\"); ",
              "Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stri"
            ],
            "visible_texts": [
              "Copy // serverless.yml - just the chromium layer config layers: chromium: path: layer compatibleRuntimes: - nodejs18.x package: artifact: layer/chromium.zip // handler.js - and pray it works const chromium = require(\"@sparticuz/chromium\"); ",
              "Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stri"
            ],
            "match_texts": [
              "Copy // serverless.yml - just the chromium layer config layers: chromium: path: layer compatibleRuntimes: - nodejs18.x package: artifact: layer/chromium.zip // handler.js - and pray it works const chromium = require(\"@sparticuz/chromium\"); const puppeteer = require(\"puppeteer-core\"); exports.handler = async (event) => { // Configure for Lambda's weird environment chromium.setHeadlessMode = true; chromium.setGraphicsMode = false; const browser = await puppeteer.launch({ args: chromium.args, defaultViewport: chromium.defaultViewport, executablePath: await chromium.executablePath(), headless: chromium.headless, }); // Hope Chrome doesn't crash... const page = await browser.newPage(); await page.goto(event.url); const screenshot = await page.screenshot(); await browser.close(); return screenshot; };",
              "Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stringify({ url: \"https://example.com\" }) }); const png = await response.arrayBuffer(); // png contains your screenshot bytes"
            ],
            "visible_match_texts": [
              "Copy // serverless.yml - just the chromium layer config layers: chromium: path: layer compatibleRuntimes: - nodejs18.x package: artifact: layer/chromium.zip // handler.js - and pray it works const chromium = require(\"@sparticuz/chromium\"); const puppeteer = require(\"puppeteer-core\"); exports.handler = async (event) => { // Configure for Lambda's weird environment chromium.setHeadlessMode = true; chromium.setGraphicsMode = false; const browser = await puppeteer.launch({ args: chromium.args, defaultViewport: chromium.defaultViewport, executablePath: await chromium.executablePath(), headless: chromium.headless, }); // Hope Chrome doesn't crash... const page = await browser.newPage(); await page.goto(event.url); const screenshot = await page.screenshot(); await browser.close(); return screenshot; };",
              "Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stringify({ url: \"https://example.com\" }) }); const png = await response.arrayBuffer(); // png contains your screenshot bytes"
            ]
          },
          ".landing-page": {
            "count": 1,
            "visible_count": 1,
            "texts": [
              "Playwright for Serverless Chromium is ~280MB. Lambda's limit is 250MB. Vercel caps at 50MB. Cloudflare Workers can't run it at all. We solved all of these. Recognize These? Unzipped size must be smaller than 262144000 bytes Chromium is ~280"
            ],
            "visible_texts": [
              "Playwright for Serverless Chromium is ~280MB. Lambda's limit is 250MB. Vercel caps at 50MB. Cloudflare Workers can't run it at all. We solved all of these. Recognize These? Unzipped size must be smaller than 262144000 bytes Chromium is ~280"
            ],
            "match_texts": [
              "Playwright for Serverless Chromium is ~280MB. Lambda's limit is 250MB. Vercel caps at 50MB. Cloudflare Workers can't run it at all. We solved all of these. Recognize These? Unzipped size must be smaller than 262144000 bytes Chromium is ~280MB. Lambda's unzipped limit is 250MB. The math doesn't work, no matter how clever you get with compression. spawn ENOMEM Lambda ran out of memory trying to spawn Chrome. You need 512MB minimum, 1-2GB recommended. That gets expensive. error while loading shared libraries: libnss3.so Missing Linux dependencies on Amazon Linux. Time to debug Lambda layers and figure out which .so files are missing. Task timed out after 30 seconds Cold start + Brotli decompression + Chrome launch + page render exceeded API Gateway's limit. And that's on a fast day. If you've seen these errors, you know the pain. We built Riddle so you don't have to deal with this anymore. The Serverless Browser Problem Every major serverless platform has limits that make headless Chrome difficult or impossible: AWS Lambda 250MB unzipped limit Chromium is 280MB. @sparticuz/chromium compresses to ~45MB but decompresses to /tmp on every cold start. VPC-enabled Lambdas can take 5+ minutes. Vercel Functions 50MB compressed limit Even aggressive compression doesn't work. Developers are stuck pinning ancient Puppeteer versions from 2021. Modern features are out of reach. Cloudflare Workers V8 isolates, no browser Workers run JavaScript, not containers. You literally cannot run Chrome. Cloudflare's Browser Rendering API exists but is rate-limited to 2/min. The @sparticuz/chromium Trap Over 300,000 developers download @sparticuz/chromium every week. Most of them are frustrated within hours. Version Pinning Hell Chromium version must match Puppeteer version must match Node.js runtime. AWS updates break your stack. You spend hours debugging compatibility. Cold Start Penalty Brotli decompression on every cold start. 5-10 seconds before your function even starts working. In VPCs, this can take minutes. Memory Instability Chrome loves RAM. Lambda doesn't have much. Your function crashes randomly on complex pages. \"spawn ENOMEM\" becomes your new nemesis. Runtime Update Roulette April 2024's Node.js 18.20 update froze Puppeteer for thousands of developers. You're one security patch away from a broken production system. As one developer put it: \"We had been burned by using third-party chromium packages and things could easily break with one security patch from AWS.\" The Solution: Don't Run Chrome in Lambda Instead of fighting Lambda limits, call an API. We run Playwright in proper infrastructure so you don't have to. 1 Your Lambda calls Riddle POST a URL or a Playwright script. We handle the browser. 2 We run Chrome properly No size limits, no memory constraints, no cold start decompression. Just Chrome running on real infrastructure. 3 You get your screenshot PNG bytes come back directly for simple captures. Use async polling or webhooks for longer workflows. Before and After Before: Lambda with @sparticuz/chromium (50+ lines of config) Copy // serverless.yml - just the chromium layer config layers: chromium: path: layer compatibleRuntimes: - nodejs18.x package: artifact: layer/chromium.zip // handler.js - and pray it works const chromium = require(\"@sparticuz/chromium\"); const puppeteer = require(\"puppeteer-core\"); exports.handler = async (event) => { // Configure for Lambda's weird environment chromium.setHeadlessMode = true; chromium.setGraphicsMode = false; const browser = await puppeteer.launch({ args: chromium.args, defaultViewport: chromium.defaultViewport, executablePath: await chromium.executablePath(), headless: chromium.headless, }); // Hope Chrome doesn't crash... const page = await browser.newPage(); await page.goto(event.url); const screenshot = await page.screenshot(); await browser.close(); return screenshot; }; After: One API call Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stringify({ url: \"https://example.com\" }) }); const png = await response.arrayBuffer(); // png contains your screenshot bytes Get Started Free Full Comparison Factor Self-Hosted Lambda Riddle API Setup time Hours of debugging layers, binaries, dependencies curl command in 2 minutes Size limit 250MB (Chromium is 280MB) Not your problem Memory needed 1-2GB minimum, crashes common Whatever your Lambda actually needs Cold start 5-10s (Brotli decompression) ~2s (HTTP call) Maintenance Breaks with Node.js runtime updates We handle it Version compatibility Chromium + Puppeteer + Node.js matrix Always latest Works on Vercel Barely (ancient versions only) Yes Works on Cloudflare No Yes Cost per job ~$0.001 + compute + your debugging time from $0.004 (all-in) Read the detailed breakdown → What You Can Do Simple Screenshots POST a URL, get a PNG. Pass auth headers to screenshot staging environments and protected pages. JSON Workflows Multi-page flows, form fills, clicks, waits. Send JSON steps, no code required. Batch Screenshots Take multiple screenshots in one job. Get below $0.001 per screenshot with batching. PDF Generation Render pages to PDF with full CSS support. No more wkhtmltopdf headaches. From $0.004 per job. Under $0.001 per screenshot when batched. See pricing → Stop Fighting Lambda Create an account and get screenshots in minutes, not hours. Get Started Free View Docs"
            ],
            "visible_match_texts": [
              "Playwright for Serverless Chromium is ~280MB. Lambda's limit is 250MB. Vercel caps at 50MB. Cloudflare Workers can't run it at all. We solved all of these. Recognize These? Unzipped size must be smaller than 262144000 bytes Chromium is ~280MB. Lambda's unzipped limit is 250MB. The math doesn't work, no matter how clever you get with compression. spawn ENOMEM Lambda ran out of memory trying to spawn Chrome. You need 512MB minimum, 1-2GB recommended. That gets expensive. error while loading shared libraries: libnss3.so Missing Linux dependencies on Amazon Linux. Time to debug Lambda layers and figure out which .so files are missing. Task timed out after 30 seconds Cold start + Brotli decompression + Chrome launch + page render exceeded API Gateway's limit. And that's on a fast day. If you've seen these errors, you know the pain. We built Riddle so you don't have to deal with this anymore. The Serverless Browser Problem Every major serverless platform has limits that make headless Chrome difficult or impossible: AWS Lambda 250MB unzipped limit Chromium is 280MB. @sparticuz/chromium compresses to ~45MB but decompresses to /tmp on every cold start. VPC-enabled Lambdas can take 5+ minutes. Vercel Functions 50MB compressed limit Even aggressive compression doesn't work. Developers are stuck pinning ancient Puppeteer versions from 2021. Modern features are out of reach. Cloudflare Workers V8 isolates, no browser Workers run JavaScript, not containers. You literally cannot run Chrome. Cloudflare's Browser Rendering API exists but is rate-limited to 2/min. The @sparticuz/chromium Trap Over 300,000 developers download @sparticuz/chromium every week. Most of them are frustrated within hours. Version Pinning Hell Chromium version must match Puppeteer version must match Node.js runtime. AWS updates break your stack. You spend hours debugging compatibility. Cold Start Penalty Brotli decompression on every cold start. 5-10 seconds before your function even starts working. In VPCs, this can take minutes. Memory Instability Chrome loves RAM. Lambda doesn't have much. Your function crashes randomly on complex pages. \"spawn ENOMEM\" becomes your new nemesis. Runtime Update Roulette April 2024's Node.js 18.20 update froze Puppeteer for thousands of developers. You're one security patch away from a broken production system. As one developer put it: \"We had been burned by using third-party chromium packages and things could easily break with one security patch from AWS.\" The Solution: Don't Run Chrome in Lambda Instead of fighting Lambda limits, call an API. We run Playwright in proper infrastructure so you don't have to. 1 Your Lambda calls Riddle POST a URL or a Playwright script. We handle the browser. 2 We run Chrome properly No size limits, no memory constraints, no cold start decompression. Just Chrome running on real infrastructure. 3 You get your screenshot PNG bytes come back directly for simple captures. Use async polling or webhooks for longer workflows. Before and After Before: Lambda with @sparticuz/chromium (50+ lines of config) Copy // serverless.yml - just the chromium layer config layers: chromium: path: layer compatibleRuntimes: - nodejs18.x package: artifact: layer/chromium.zip // handler.js - and pray it works const chromium = require(\"@sparticuz/chromium\"); const puppeteer = require(\"puppeteer-core\"); exports.handler = async (event) => { // Configure for Lambda's weird environment chromium.setHeadlessMode = true; chromium.setGraphicsMode = false; const browser = await puppeteer.launch({ args: chromium.args, defaultViewport: chromium.defaultViewport, executablePath: await chromium.executablePath(), headless: chromium.headless, }); // Hope Chrome doesn't crash... const page = await browser.newPage(); await page.goto(event.url); const screenshot = await page.screenshot(); await browser.close(); return screenshot; }; After: One API call Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stringify({ url: \"https://example.com\" }) }); const png = await response.arrayBuffer(); // png contains your screenshot bytes Get Started Free Full Comparison Factor Self-Hosted Lambda Riddle API Setup time Hours of debugging layers, binaries, dependencies curl command in 2 minutes Size limit 250MB (Chromium is 280MB) Not your problem Memory needed 1-2GB minimum, crashes common Whatever your Lambda actually needs Cold start 5-10s (Brotli decompression) ~2s (HTTP call) Maintenance Breaks with Node.js runtime updates We handle it Version compatibility Chromium + Puppeteer + Node.js matrix Always latest Works on Vercel Barely (ancient versions only) Yes Works on Cloudflare No Yes Cost per job ~$0.001 + compute + your debugging time from $0.004 (all-in) Read the detailed breakdown → What You Can Do Simple Screenshots POST a URL, get a PNG. Pass auth headers to screenshot staging environments and protected pages. JSON Workflows Multi-page flows, form fills, clicks, waits. Send JSON steps, no code required. Batch Screenshots Take multiple screenshots in one job. Get below $0.001 per screenshot with batching. PDF Generation Render pages to PDF with full CSS support. No more wkhtmltopdf headaches. From $0.004 per job. Under $0.001 per screenshot when batched. See pricing → Stop Fighting Lambda Create an account and get screenshots in minutes, not hours. Get Started Free View Docs"
            ]
          }
        },
        "text_matches": {
          "text:Playwright for Serverless": true,
          "text:After: One API call": true,
          "text:POST a URL, get a PNG": true
        },
        "http_statuses": {},
        "link_statuses": {
          "a[href*='/register/'], a[href*='/docs/'], a[href*='/pricing/'], a[href*='/vs-self-hosted/']": {
            "version": "riddle-proof.link-status.v1",
            "selector": "a[href*='/register/'], a[href*='/docs/'], a[href*='/pricing/'], a[href*='/vs-self-hosted/']",
            "max_links": 20,
            "same_origin_only": true,
            "dedupe": true,
            "require_nonzero_bytes": true,
            "min_bytes": 1000,
            "allowed_content_types": [
              "text/html"
            ],
            "allowed_statuses": [
              "2xx",
              "3xx"
            ],
            "discovered_count": 12,
            "total_count": 4,
            "truncated": false,
            "ok_count": 4,
            "failed_count": 0,
            "status_counts": {
              "206": 4
            },
            "failures": [],
            "result_count": 4,
            "stored_result_count": 4,
            "omitted_result_count": 0,
            "omitted_success_count": 0,
            "results_compacted": false,
            "results": [
              {
                "url": "https://riddledc.com/docs/",
                "tag": "a",
                "text": "Docs",
                "status": 206,
                "method": "GET",
                "ok": true,
                "content_type": "text/html; charset=utf-8",
                "content_length": 1000,
                "bytes": 1000,
                "redirected": false,
                "final_url": "https://riddledc.com/docs/",
                "error": null
              },
              {
                "url": "https://riddledc.com/pricing/",
                "tag": "a",
                "text": "Pricing",
                "status": 206,
                "method": "GET",
                "ok": true,
                "content_type": "text/html; charset=utf-8",
                "content_length": 1000,
                "bytes": 1000,
                "redirected": false,
                "final_url": "https://riddledc.com/pricing/",
                "error": null
              },
              {
                "url": "https://riddledc.com/register/",
                "tag": "a",
                "text": "Sign Up",
                "status": 206,
                "method": "GET",
                "ok": true,
                "content_type": "text/html; charset=utf-8",
                "content_length": 1000,
                "bytes": 1000,
                "redirected": false,
                "final_url": "https://riddledc.com/register/",
                "error": null
              },
              {
                "url": "https://riddledc.com/vs-self-hosted/",
                "tag": "a",
                "text": "Read the detailed breakdown →",
                "status": 206,
                "method": "GET",
                "ok": true,
                "content_type": "text/html; charset=utf-8",
                "content_length": 1000,
                "bytes": 1000,
                "redirected": false,
                "final_url": "https://riddledc.com/vs-self-hosted/",
                "error": null
              }
            ]
          }
        },
        "setup_action_results": [],
        "screenshot_label": "riddle-site-v491-serverless-simple-screenshot-response-ipad-mini"
      },
      {
        "name": "ipad",
        "width": 820,
        "height": 1180,
        "url": "https://riddledc.com/serverless/",
        "route": {
          "requested": "https://riddledc.com/serverless/",
          "observed": "/serverless/",
          "expected_path": "/serverless/",
          "matched": true,
          "http_status": 200
        },
        "title": "Serverless Browser Automation - Riddle",
        "body_text_length": 6011,
        "body_text_sample": "Skip to main content Riddle Playwright for Serverless Chromium is ~280MB. Lambda's limit is 250MB. Vercel caps at 50MB. Cloudflare Workers can't run it at all. We solved all of these. Recognize These? Unzipped size must be smaller than 262144000 bytes Chromium is ~280MB. Lambda's unzipped limit is 250MB. The math doesn't work, no matter how clever you get with compression. spawn ENOMEM Lambda ran out of memory trying to spawn Chrome. You need 512MB minimum, 1-2GB recommended. That gets expensive. error while loading shared libraries: libnss3.so Missing Linux dependencies on Amazon Linux. Time to debug Lambda layers and figure out which .so files are missing. Task timed out after 30 seconds Cold start + Brotli decompression + Chrome launch + page render exceeded API Gateway's limit. And that's on a fast day. If you've seen these errors, you know the pain. We built Riddle so you don't have to deal with this anymore. The Serverless Browser Problem Every major serverless platform has limits that make headless Chrome difficult or impossible: AWS Lambda 250MB unzipped limit Chromium is 280MB. @sparticuz/chromium compresses to ~45MB but decompresses to /tmp on every cold start. VPC-enabled Lambdas can take 5+ minutes. Vercel Functions 50MB compressed limit Even aggressive compression doesn't work. Developers are stuck pinning ancient Puppeteer versions from 2021. Modern features are out of reach. Cloudflare Workers V8 isolates, no browser Workers run JavaScript, not containers. You literally cannot run Chrome. Cloudflare's Browser Rendering API exists but is rate-limited to 2/min. The @sparticuz/chromium Trap Over 300,000 developers download @sparticuz/chromium every week. Most of them are frustrated within hours. Version Pinning Hell Chromium version must match Puppeteer version must match Node.js runtime. AWS updates break your stack. You spend hours debugging compatibility. Cold Start Penalty Brotli decompression on every cold start. 5-10 seconds before your function even starts working. In VPCs, this can take minutes. Memory Instability Chrome loves RAM. Lambda doesn't have much. Your function crashes randomly on complex pages. \"spawn ENOMEM\" becomes your new nemesis. Runtime Update Roulette April 2024's Node.js 18.20 update froze Puppeteer for thousands of developers. You're one security patch away from a broken production system. As one developer put it: \"We had been burned by using third-party chromium packages and things could easily break with one security patch from AWS.\" The Solution: Don't Run Chrome in Lambda Instead of fighting Lambda limits, call an API. We run Playwright in proper infrastructure so you don't have to. 1 Your Lambda calls Riddle POST a URL or a Playwright script. We handle the browser. 2 We run Chrome properly No size limits, no memory constraints, no cold start decompression. Just Chrome running on real infrastructure. 3 You get your screenshot PNG bytes come back directly for simple captures. Use async polling or webhooks for longer workflows. Before and After Before: Lambda with @sparticuz/chromium (50+ lines of config) Copy // serverless.yml - just the chromium layer config layers: chromium: path: layer compatibleRuntimes: - nodejs18.x package: artifact: layer/chromium.zip // handler.js - and pray it works const chromium = require(\"@sparticuz/chromium\"); const puppeteer = require(\"puppeteer-core\"); exports.handler = async (event) => { // Configure for Lambda's weird environment chromium.setHeadlessMode = true; chromium.setGraphicsMode = false; const browser = await puppeteer.launch({ args: chromium.args, defaultViewport: chromium.defaultViewport, executablePath: await chromium.executablePath(), headless: chromium.headless, }); // Hope Chrome doesn't crash... const page = await browser.newPage(); await page.goto(event.url); const screenshot = await page.screenshot(); await browser.close(); return screenshot; }; After: One API call Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stringify({ url: \"https://example.com\" }) }); const png = await response.arrayBuffer(); // png contains your screenshot bytes Get Started Free Full Comparison Factor Self-Hosted Lambda Riddle API Setup time Hours of debugging layers, binaries, dependencies curl command in 2 minutes Size limit 250MB (Chromium is 280MB) Not your problem Memory needed 1-2GB minimum, crashes common Whatever your Lambda actually needs Cold start 5-10s (Brotli decompression) ~2s (HTTP call) Maintenance Breaks with Node.js runtime updates We handle it Version compatibility Chromium + Puppeteer + Node.js matrix Always latest Works on Vercel Barely (ancient versions only) Yes Works on Cloudflare No Yes Cost per job ~$0.001 + compute + your debugging time from $0.004 (all-in) Read the detailed breakdown → What You Can Do Simple Screenshots POST a URL, get a PNG. Pass auth headers to screenshot staging environments and protected pages. JSON Workflows Multi-page flows, form fills, clicks, waits. Send JSON steps, no code required. Batch Screenshots Take multiple screenshots in one job. Get below $0.001 per screenshot with batching. PDF Generation Render pages to PDF with full CSS support. No more wkhtmltopdf headaches. From $0.004 per job. Under $0.001 per screenshot when batched. See pricing → Stop Fighting Lambda Create an account and get screenshots in minutes, not hours. Get Started Free View Docs Riddle Distributed Computing Product Riddle Proof Good Catch Diary Pricing Maximize Value Documentation MCP Recipes Playground Use Cases AI Agents Agent Guide Serverless Authenticated Pages vs Self-Hosted Blog Evidence Over Summaries Frontend Dev Without a Browser E2E Testing Tips Chatty Agents (Part I) Batching Heuristics (Part II) Vision Agents Teaching AI to Ski Support FAQ Status Contact Legal Terms Privacy © 2026 Riddle. All rights reserved.",
        "scroll_width": 820,
        "client_width": 820,
        "overflow_px": 0,
        "bounds_overflow_px": 0,
        "overflow_offenders": [],
        "selectors": {
          ".landing-page": {
            "count": 1,
            "visible_count": 1
          },
          ".code-block-wrapper": {
            "count": 2,
            "visible_count": 2
          }
        },
        "frames": {},
        "text_sequences": {
          ".code-block-wrapper": {
            "count": 2,
            "visible_count": 2,
            "texts": [
              "Copy // serverless.yml - just the chromium layer config layers: chromium: path: layer compatibleRuntimes: - nodejs18.x package: artifact: layer/chromium.zip // handler.js - and pray it works const chromium = require(\"@sparticuz/chromium\"); ",
              "Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stri"
            ],
            "visible_texts": [
              "Copy // serverless.yml - just the chromium layer config layers: chromium: path: layer compatibleRuntimes: - nodejs18.x package: artifact: layer/chromium.zip // handler.js - and pray it works const chromium = require(\"@sparticuz/chromium\"); ",
              "Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stri"
            ],
            "match_texts": [
              "Copy // serverless.yml - just the chromium layer config layers: chromium: path: layer compatibleRuntimes: - nodejs18.x package: artifact: layer/chromium.zip // handler.js - and pray it works const chromium = require(\"@sparticuz/chromium\"); const puppeteer = require(\"puppeteer-core\"); exports.handler = async (event) => { // Configure for Lambda's weird environment chromium.setHeadlessMode = true; chromium.setGraphicsMode = false; const browser = await puppeteer.launch({ args: chromium.args, defaultViewport: chromium.defaultViewport, executablePath: await chromium.executablePath(), headless: chromium.headless, }); // Hope Chrome doesn't crash... const page = await browser.newPage(); await page.goto(event.url); const screenshot = await page.screenshot(); await browser.close(); return screenshot; };",
              "Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stringify({ url: \"https://example.com\" }) }); const png = await response.arrayBuffer(); // png contains your screenshot bytes"
            ],
            "visible_match_texts": [
              "Copy // serverless.yml - just the chromium layer config layers: chromium: path: layer compatibleRuntimes: - nodejs18.x package: artifact: layer/chromium.zip // handler.js - and pray it works const chromium = require(\"@sparticuz/chromium\"); const puppeteer = require(\"puppeteer-core\"); exports.handler = async (event) => { // Configure for Lambda's weird environment chromium.setHeadlessMode = true; chromium.setGraphicsMode = false; const browser = await puppeteer.launch({ args: chromium.args, defaultViewport: chromium.defaultViewport, executablePath: await chromium.executablePath(), headless: chromium.headless, }); // Hope Chrome doesn't crash... const page = await browser.newPage(); await page.goto(event.url); const screenshot = await page.screenshot(); await browser.close(); return screenshot; };",
              "Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stringify({ url: \"https://example.com\" }) }); const png = await response.arrayBuffer(); // png contains your screenshot bytes"
            ]
          },
          ".landing-page": {
            "count": 1,
            "visible_count": 1,
            "texts": [
              "Playwright for Serverless Chromium is ~280MB. Lambda's limit is 250MB. Vercel caps at 50MB. Cloudflare Workers can't run it at all. We solved all of these. Recognize These? Unzipped size must be smaller than 262144000 bytes Chromium is ~280"
            ],
            "visible_texts": [
              "Playwright for Serverless Chromium is ~280MB. Lambda's limit is 250MB. Vercel caps at 50MB. Cloudflare Workers can't run it at all. We solved all of these. Recognize These? Unzipped size must be smaller than 262144000 bytes Chromium is ~280"
            ],
            "match_texts": [
              "Playwright for Serverless Chromium is ~280MB. Lambda's limit is 250MB. Vercel caps at 50MB. Cloudflare Workers can't run it at all. We solved all of these. Recognize These? Unzipped size must be smaller than 262144000 bytes Chromium is ~280MB. Lambda's unzipped limit is 250MB. The math doesn't work, no matter how clever you get with compression. spawn ENOMEM Lambda ran out of memory trying to spawn Chrome. You need 512MB minimum, 1-2GB recommended. That gets expensive. error while loading shared libraries: libnss3.so Missing Linux dependencies on Amazon Linux. Time to debug Lambda layers and figure out which .so files are missing. Task timed out after 30 seconds Cold start + Brotli decompression + Chrome launch + page render exceeded API Gateway's limit. And that's on a fast day. If you've seen these errors, you know the pain. We built Riddle so you don't have to deal with this anymore. The Serverless Browser Problem Every major serverless platform has limits that make headless Chrome difficult or impossible: AWS Lambda 250MB unzipped limit Chromium is 280MB. @sparticuz/chromium compresses to ~45MB but decompresses to /tmp on every cold start. VPC-enabled Lambdas can take 5+ minutes. Vercel Functions 50MB compressed limit Even aggressive compression doesn't work. Developers are stuck pinning ancient Puppeteer versions from 2021. Modern features are out of reach. Cloudflare Workers V8 isolates, no browser Workers run JavaScript, not containers. You literally cannot run Chrome. Cloudflare's Browser Rendering API exists but is rate-limited to 2/min. The @sparticuz/chromium Trap Over 300,000 developers download @sparticuz/chromium every week. Most of them are frustrated within hours. Version Pinning Hell Chromium version must match Puppeteer version must match Node.js runtime. AWS updates break your stack. You spend hours debugging compatibility. Cold Start Penalty Brotli decompression on every cold start. 5-10 seconds before your function even starts working. In VPCs, this can take minutes. Memory Instability Chrome loves RAM. Lambda doesn't have much. Your function crashes randomly on complex pages. \"spawn ENOMEM\" becomes your new nemesis. Runtime Update Roulette April 2024's Node.js 18.20 update froze Puppeteer for thousands of developers. You're one security patch away from a broken production system. As one developer put it: \"We had been burned by using third-party chromium packages and things could easily break with one security patch from AWS.\" The Solution: Don't Run Chrome in Lambda Instead of fighting Lambda limits, call an API. We run Playwright in proper infrastructure so you don't have to. 1 Your Lambda calls Riddle POST a URL or a Playwright script. We handle the browser. 2 We run Chrome properly No size limits, no memory constraints, no cold start decompression. Just Chrome running on real infrastructure. 3 You get your screenshot PNG bytes come back directly for simple captures. Use async polling or webhooks for longer workflows. Before and After Before: Lambda with @sparticuz/chromium (50+ lines of config) Copy // serverless.yml - just the chromium layer config layers: chromium: path: layer compatibleRuntimes: - nodejs18.x package: artifact: layer/chromium.zip // handler.js - and pray it works const chromium = require(\"@sparticuz/chromium\"); const puppeteer = require(\"puppeteer-core\"); exports.handler = async (event) => { // Configure for Lambda's weird environment chromium.setHeadlessMode = true; chromium.setGraphicsMode = false; const browser = await puppeteer.launch({ args: chromium.args, defaultViewport: chromium.defaultViewport, executablePath: await chromium.executablePath(), headless: chromium.headless, }); // Hope Chrome doesn't crash... const page = await browser.newPage(); await page.goto(event.url); const screenshot = await page.screenshot(); await browser.close(); return screenshot; }; After: One API call Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stringify({ url: \"https://example.com\" }) }); const png = await response.arrayBuffer(); // png contains your screenshot bytes Get Started Free Full Comparison Factor Self-Hosted Lambda Riddle API Setup time Hours of debugging layers, binaries, dependencies curl command in 2 minutes Size limit 250MB (Chromium is 280MB) Not your problem Memory needed 1-2GB minimum, crashes common Whatever your Lambda actually needs Cold start 5-10s (Brotli decompression) ~2s (HTTP call) Maintenance Breaks with Node.js runtime updates We handle it Version compatibility Chromium + Puppeteer + Node.js matrix Always latest Works on Vercel Barely (ancient versions only) Yes Works on Cloudflare No Yes Cost per job ~$0.001 + compute + your debugging time from $0.004 (all-in) Read the detailed breakdown → What You Can Do Simple Screenshots POST a URL, get a PNG. Pass auth headers to screenshot staging environments and protected pages. JSON Workflows Multi-page flows, form fills, clicks, waits. Send JSON steps, no code required. Batch Screenshots Take multiple screenshots in one job. Get below $0.001 per screenshot with batching. PDF Generation Render pages to PDF with full CSS support. No more wkhtmltopdf headaches. From $0.004 per job. Under $0.001 per screenshot when batched. See pricing → Stop Fighting Lambda Create an account and get screenshots in minutes, not hours. Get Started Free View Docs"
            ],
            "visible_match_texts": [
              "Playwright for Serverless Chromium is ~280MB. Lambda's limit is 250MB. Vercel caps at 50MB. Cloudflare Workers can't run it at all. We solved all of these. Recognize These? Unzipped size must be smaller than 262144000 bytes Chromium is ~280MB. Lambda's unzipped limit is 250MB. The math doesn't work, no matter how clever you get with compression. spawn ENOMEM Lambda ran out of memory trying to spawn Chrome. You need 512MB minimum, 1-2GB recommended. That gets expensive. error while loading shared libraries: libnss3.so Missing Linux dependencies on Amazon Linux. Time to debug Lambda layers and figure out which .so files are missing. Task timed out after 30 seconds Cold start + Brotli decompression + Chrome launch + page render exceeded API Gateway's limit. And that's on a fast day. If you've seen these errors, you know the pain. We built Riddle so you don't have to deal with this anymore. The Serverless Browser Problem Every major serverless platform has limits that make headless Chrome difficult or impossible: AWS Lambda 250MB unzipped limit Chromium is 280MB. @sparticuz/chromium compresses to ~45MB but decompresses to /tmp on every cold start. VPC-enabled Lambdas can take 5+ minutes. Vercel Functions 50MB compressed limit Even aggressive compression doesn't work. Developers are stuck pinning ancient Puppeteer versions from 2021. Modern features are out of reach. Cloudflare Workers V8 isolates, no browser Workers run JavaScript, not containers. You literally cannot run Chrome. Cloudflare's Browser Rendering API exists but is rate-limited to 2/min. The @sparticuz/chromium Trap Over 300,000 developers download @sparticuz/chromium every week. Most of them are frustrated within hours. Version Pinning Hell Chromium version must match Puppeteer version must match Node.js runtime. AWS updates break your stack. You spend hours debugging compatibility. Cold Start Penalty Brotli decompression on every cold start. 5-10 seconds before your function even starts working. In VPCs, this can take minutes. Memory Instability Chrome loves RAM. Lambda doesn't have much. Your function crashes randomly on complex pages. \"spawn ENOMEM\" becomes your new nemesis. Runtime Update Roulette April 2024's Node.js 18.20 update froze Puppeteer for thousands of developers. You're one security patch away from a broken production system. As one developer put it: \"We had been burned by using third-party chromium packages and things could easily break with one security patch from AWS.\" The Solution: Don't Run Chrome in Lambda Instead of fighting Lambda limits, call an API. We run Playwright in proper infrastructure so you don't have to. 1 Your Lambda calls Riddle POST a URL or a Playwright script. We handle the browser. 2 We run Chrome properly No size limits, no memory constraints, no cold start decompression. Just Chrome running on real infrastructure. 3 You get your screenshot PNG bytes come back directly for simple captures. Use async polling or webhooks for longer workflows. Before and After Before: Lambda with @sparticuz/chromium (50+ lines of config) Copy // serverless.yml - just the chromium layer config layers: chromium: path: layer compatibleRuntimes: - nodejs18.x package: artifact: layer/chromium.zip // handler.js - and pray it works const chromium = require(\"@sparticuz/chromium\"); const puppeteer = require(\"puppeteer-core\"); exports.handler = async (event) => { // Configure for Lambda's weird environment chromium.setHeadlessMode = true; chromium.setGraphicsMode = false; const browser = await puppeteer.launch({ args: chromium.args, defaultViewport: chromium.defaultViewport, executablePath: await chromium.executablePath(), headless: chromium.headless, }); // Hope Chrome doesn't crash... const page = await browser.newPage(); await page.goto(event.url); const screenshot = await page.screenshot(); await browser.close(); return screenshot; }; After: One API call Copy // That's it. No layers, no binaries, no config. const response = await fetch(\"https://api.riddledc.com/v1/run\", { method: \"POST\", headers: { \"Authorization\": \"Bearer YOUR_API_KEY\", \"Content-Type\": \"application/json\" }, body: JSON.stringify({ url: \"https://example.com\" }) }); const png = await response.arrayBuffer(); // png contains your screenshot bytes Get Started Free Full Comparison Factor Self-Hosted Lambda Riddle API Setup time Hours of debugging layers, binaries, dependencies curl command in 2 minutes Size limit 250MB (Chromium is 280MB) Not your problem Memory needed 1-2GB minimum, crashes common Whatever your Lambda actually needs Cold start 5-10s (Brotli decompression) ~2s (HTTP call) Maintenance Breaks with Node.js runtime updates We handle it Version compatibility Chromium + Puppeteer + Node.js matrix Always latest Works on Vercel Barely (ancient versions only) Yes Works on Cloudflare No Yes Cost per job ~$0.001 + compute + your debugging time from $0.004 (all-in) Read the detailed breakdown → What You Can Do Simple Screenshots POST a URL, get a PNG. Pass auth headers to screenshot staging environments and protected pages. JSON Workflows Multi-page flows, form fills, clicks, waits. Send JSON steps, no code required. Batch Screenshots Take multiple screenshots in one job. Get below $0.001 per screenshot with batching. PDF Generation Render pages to PDF with full CSS support. No more wkhtmltopdf headaches. From $0.004 per job. Under $0.001 per screenshot when batched. See pricing → Stop Fighting Lambda Create an account and get screenshots in minutes, not hours. Get Started Free View Docs"
            ]
          }
        },
        "text_matches": {
          "text:Playwright for Serverless": true,
          "text:After: One API call": true,
          "text:POST a URL, get a PNG": true
        },
        "http_statuses": {},
        "link_statuses": {
          "a[href*='/register/'], a[href*='/docs/'], a[href*='/pricing/'], a[href*='/vs-self-hosted/']": {
            "version": "riddle-proof.link-status.v1",
            "selector": "a[href*='/register/'], a[href*='/docs/'], a[href*='/pricing/'], a[href*='/vs-self-hosted/']",
            "max_links": 20,
            "same_origin_only": true,
            "dedupe": true,
            "require_nonzero_bytes": true,
            "min_bytes": 1000,
            "allowed_content_types": [
              "text/html"
            ],
            "allowed_statuses": [
              "2xx",
              "3xx"
            ],
            "discovered_count": 12,
            "total_count": 4,
            "truncated": false,
            "ok_count": 4,
            "failed_count": 0,
            "status_counts": {
              "206": 4
            },
            "failures": [],
            "result_count": 4,
            "stored_result_count": 4,
            "omitted_result_count": 0,
            "omitted_success_count": 0,
            "results_compacted": false,
            "results": [
              {
                "url": "https://riddledc.com/docs/",
                "tag": "a",
                "text": "Docs",
                "status": 206,
                "method": "GET",
                "ok": true,
                "content_type": "text/html; charset=utf-8",
                "content_length": 1000,
                "bytes": 1000,
                "redirected": false,
                "final_url": "https://riddledc.com/docs/",
                "error": null
              },
              {
                "url": "https://riddledc.com/pricing/",
                "tag": "a",
                "text": "Pricing",
                "status": 206,
                "method": "GET",
                "ok": true,
                "content_type": "text/html; charset=utf-8",
                "content_length": 1000,
                "bytes": 1000,
                "redirected": false,
                "final_url": "https://riddledc.com/pricing/",
                "error": null
              },
              {
                "url": "https://riddledc.com/register/",
                "tag": "a",
                "text": "Sign Up",
                "status": 206,
                "method": "GET",
                "ok": true,
                "content_type": "text/html; charset=utf-8",
                "content_length": 1000,
                "bytes": 1000,
                "redirected": false,
                "final_url": "https://riddledc.com/register/",
                "error": null
              },
              {
                "url": "https://riddledc.com/vs-self-hosted/",
                "tag": "a",
                "text": "Read the detailed breakdown →",
                "status": 206,
                "method": "GET",
                "ok": true,
                "content_type": "text/html; charset=utf-8",
                "content_length": 1000,
                "bytes": 1000,
                "redirected": false,
                "final_url": "https://riddledc.com/vs-self-hosted/",
                "error": null
              }
            ]
          }
        },
        "setup_action_results": [],
        "screenshot_label": "riddle-site-v491-serverless-simple-screenshot-response-ipad"
      }
    ],
    "console": {
      "events": [],
      "fatal_count": 0
    },
    "page_errors": [],
    "dialogs": [],
    "network_mocks": [],
    "dom_summary": {
      "expected_viewport_count": 4,
      "viewport_count": 4,
      "partial": false,
      "routes": [
        {
          "requested": "https://riddledc.com/serverless/",
          "observed": "/serverless/",
          "expected_path": "/serverless/",
          "matched": true,
          "http_status": 200
        },
        {
          "requested": "https://riddledc.com/serverless/",
          "observed": "/serverless/",
          "expected_path": "/serverless/",
          "matched": true,
          "http_status": 200
        },
        {
          "requested": "https://riddledc.com/serverless/",
          "observed": "/serverless/",
          "expected_path": "/serverless/",
          "matched": true,
          "http_status": 200
        },
        {
          "requested": "https://riddledc.com/serverless/",
          "observed": "/serverless/",
          "expected_path": "/serverless/",
          "matched": true,
          "http_status": 200
        }
      ],
      "titles": [
        "Serverless Browser Automation - Riddle",
        "Serverless Browser Automation - Riddle",
        "Serverless Browser Automation - Riddle",
        "Serverless Browser Automation - Riddle"
      ],
      "overflow_px": [
        0,
        0,
        0,
        0
      ],
      "bounds_overflow_px": [
        0,
        0,
        0,
        0
      ],
      "overflow_offender_counts": [
        0,
        0,
        0,
        0
      ],
      "frames": [
        {
          "viewport": "desktop",
          "selectors": []
        },
        {
          "viewport": "phone",
          "selectors": []
        },
        {
          "viewport": "ipad-mini",
          "selectors": []
        },
        {
          "viewport": "ipad",
          "selectors": []
        }
      ],
      "http_status": [],
      "link_status": [
        {
          "viewport": "desktop",
          "selectors": [
            {
              "selector": "a[href*='/register/'], a[href*='/docs/'], a[href*='/pricing/'], a[href*='/vs-self-hosted/']",
              "total_count": 4,
              "ok_count": 4,
              "failed_count": 0,
              "truncated": false
            }
          ]
        },
        {
          "viewport": "phone",
          "selectors": [
            {
              "selector": "a[href*='/register/'], a[href*='/docs/'], a[href*='/pricing/'], a[href*='/vs-self-hosted/']",
              "total_count": 4,
              "ok_count": 4,
              "failed_count": 0,
              "truncated": false
            }
          ]
        },
        {
          "viewport": "ipad-mini",
          "selectors": [
            {
              "selector": "a[href*='/register/'], a[href*='/docs/'], a[href*='/pricing/'], a[href*='/vs-self-hosted/']",
              "total_count": 4,
              "ok_count": 4,
              "failed_count": 0,
              "truncated": false
            }
          ]
        },
        {
          "viewport": "ipad",
          "selectors": [
            {
              "selector": "a[href*='/register/'], a[href*='/docs/'], a[href*='/pricing/'], a[href*='/vs-self-hosted/']",
              "total_count": 4,
              "ok_count": 4,
              "failed_count": 0,
              "truncated": false
            }
          ]
        }
      ],
      "route_inventory": [],
      "network_mock_count": 0,
      "network_mock_hit_count": 0,
      "dialog_count": 0,
      "dialog_accept_count": 0,
      "dialog_dismiss_count": 0
    }
  }
}