DEV Community

Custodia-Admin
Custodia-Admin

Posted on

How to Take Screenshots in AWS Lambda (Without Puppeteer Crashing)

How to Take Screenshots in AWS Lambda (Without Puppeteer Crashing)

You're building a serverless app. A user triggers a Lambda to generate a screenshot. Puppeteer launches... and crashes.

Why?

Lambda's constraints:

  • No native Chromium — You need custom layers. One layer is 50–100MB. Uncompressed, Chromium is 200MB+.
  • Memory limits — Each Puppeteer instance eats 150–300MB. Two simultaneous requests = your Lambda runs out of memory.
  • Timeout hell — Puppeteer cold starts take 5–10 seconds. Your Lambda times out at 15 seconds by default.
  • Layer management — Keeping Chromium binaries up-to-date across deployments is painful.
  • Cost — You pay for CPU time for every second the browser is spinning up and running.

Self-hosted Puppeteer on Lambda is expensive and unreliable.

There's a simpler pattern: replace Puppeteer with an API call.

One HTTP request. Screenshot back in 1–2 seconds. No Chromium. No layers. No crashes.

Here's how to take screenshots in AWS Lambda without managing a browser.

The Problem: Puppeteer in Lambda Is Fragile

A typical Lambda screenshot function looks like this:

// Puppeteer on Lambda: fragile and slow
const puppeteer = require('puppeteer');

exports.handler = async (event) => {
  let browser;
  try {
    browser = await puppeteer.launch({
      headless: true,
      args: ['--no-sandbox', '--disable-setuid-sandbox']
    });

    const page = await browser.newPage();
    await page.goto(event.url, { waitUntil: 'networkidle2' });
    const screenshot = await page.screenshot();

    return {
      statusCode: 200,
      body: JSON.stringify({ message: 'Screenshot taken' })
    };
  } catch (error) {
    console.error(error);
    return {
      statusCode: 500,
      body: JSON.stringify({ error: error.message })
    };
  } finally {
    if (browser) await browser.close();
  }
};
Enter fullscreen mode Exit fullscreen mode

What goes wrong:

  1. Cold start — First invocation takes 10+ seconds (Chromium launch + layer extraction)
  2. Memory spike — Puppeteer + Chromium consumes 200–400MB, limiting concurrency
  3. Timeout — If navigation is slow, the function times out before returning
  4. Layer management — You're responsible for keeping Chromium binaries up-to-date
  5. Crashes — Puppeteer sometimes hangs or crashes on Lambda due to signal handling

The Solution: Screenshot API Instead

Replace Puppeteer with a simple HTTP POST:

// AWS Lambda with PageBolt API: simple and reliable
const https = require('https');

exports.handler = async (event) => {
  const { url } = event;

  if (!url) {
    return {
      statusCode: 400,
      body: JSON.stringify({ error: 'url is required' })
    };
  }

  try {
    const result = await takeScreenshot(url);

    return {
      statusCode: 200,
      body: JSON.stringify({
        message: 'Screenshot taken',
        url: result.url
      })
    };
  } catch (error) {
    console.error(error);
    return {
      statusCode: 500,
      body: JSON.stringify({ error: error.message })
    };
  }
};

function takeScreenshot(url) {
  return new Promise((resolve, reject) => {
    const payload = JSON.stringify({
      url: url,
      format: 'png',
      width: 1280,
      height: 720,
      fullPage: false
    });

    const options = {
      hostname: 'api.pagebolt.dev',
      path: '/v1/screenshot',
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.PAGEBOLT_API_KEY}`,
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(payload)
      }
    };

    const req = https.request(options, (res) => {
      let data = Buffer.alloc(0);

      res.on('data', (chunk) => {
        data = Buffer.concat([data, chunk]);
      });

      res.on('end', () => {
        if (res.statusCode === 200) {
          resolve({ url: url, statusCode: 200 });
        } else {
          reject(new Error(`API error ${res.statusCode}`));
        }
      });
    });

    req.on('error', reject);
    req.write(payload);
    req.end();
  });
}
Enter fullscreen mode Exit fullscreen mode

That's it. No Puppeteer. No Chromium layer. No crashes.

Complete Serverless Example: Save Screenshots to S3

Here's a production-ready Lambda that takes a screenshot and uploads it to S3:

// lambda-screenshot-s3.js
const https = require('https');
const AWS = require('aws-sdk');

const s3 = new AWS.S3();

exports.handler = async (event) => {
  const { url, bucket, key } = event;

  // Validate input
  if (!url || !bucket || !key) {
    return {
      statusCode: 400,
      body: JSON.stringify({
        error: 'url, bucket, and key are required'
      })
    };
  }

  try {
    // 1. Take screenshot via API
    const screenshotBuffer = await takeScreenshot(url);

    // 2. Upload to S3
    const s3Params = {
      Bucket: bucket,
      Key: key,
      Body: screenshotBuffer,
      ContentType: 'image/png'
    };

    await s3.upload(s3Params).promise();

    // 3. Generate S3 URL
    const s3Url = `https://${bucket}.s3.amazonaws.com/${key}`;

    return {
      statusCode: 200,
      body: JSON.stringify({
        message: 'Screenshot saved to S3',
        url: s3Url,
        s3Key: key
      })
    };
  } catch (error) {
    console.error('Error:', error);
    return {
      statusCode: 500,
      body: JSON.stringify({ error: error.message })
    };
  }
};

function takeScreenshot(url) {
  return new Promise((resolve, reject) => {
    const payload = JSON.stringify({
      url: url,
      format: 'png',
      width: 1280,
      height: 720,
      fullPage: true,
      blockAds: true,
      blockBanners: true
    });

    const options = {
      hostname: 'api.pagebolt.dev',
      path: '/v1/screenshot',
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.PAGEBOLT_API_KEY}`,
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(payload),
        'User-Agent': 'AWS-Lambda/Node.js'
      },
      timeout: 30000 // 30 second timeout
    };

    const req = https.request(options, (res) => {
      let data = Buffer.alloc(0);

      res.on('data', (chunk) => {
        data = Buffer.concat([data, chunk]);
      });

      res.on('end', () => {
        if (res.statusCode === 200) {
          resolve(data);
        } else {
          reject(new Error(`API error ${res.statusCode}`));
        }
      });
    });

    req.on('error', reject);
    req.on('timeout', () => {
      req.destroy();
      reject(new Error('Request timeout'));
    });

    req.write(payload);
    req.end();
  });
}
Enter fullscreen mode Exit fullscreen mode

Deployment: Lambda Configuration

Create the Lambda with these settings:

# 1. Create the function
aws lambda create-function \
  --function-name screenshot-to-s3 \
  --runtime nodejs18.x \
  --role arn:aws:iam::ACCOUNT_ID:role/lambda-basic-execution \
  --handler lambda-screenshot-s3.handler \
  --zip-file fileb://function.zip

# 2. Add S3 permissions
aws lambda add-permission \
  --function-name screenshot-to-s3 \
  --statement-id AllowS3Access \
  --action lambda:InvokeFunction \
  --principal s3.amazonaws.com

# 3. Set environment variable for API key
aws lambda update-function-configuration \
  --function-name screenshot-to-s3 \
  --environment Variables={PAGEBOLT_API_KEY=YOUR_API_KEY}
Enter fullscreen mode Exit fullscreen mode

Lambda configuration:

  • Memory: 256MB (plenty for API calls; Puppeteer would need 512MB+)
  • Timeout: 30 seconds (API calls complete in 1–5 seconds)
  • Ephemeral storage: Default 512MB

Test It

# Invoke the function
aws lambda invoke \
  --function-name screenshot-to-s3 \
  --payload '{"url":"https://example.com","bucket":"my-bucket","key":"example-screenshot.png"}' \
  --cli-binary-format raw-in-base64-out \
  response.json

cat response.json
Enter fullscreen mode Exit fullscreen mode

Output:

{
  "statusCode": 200,
  "body": "{\"message\":\"Screenshot saved to S3\",\"url\":\"https://my-bucket.s3.amazonaws.com/example-screenshot.png\",\"s3Key\":\"example-screenshot.png\"}"
}
Enter fullscreen mode Exit fullscreen mode

Cost Breakdown: API vs Self-Hosted

Metric Puppeteer on Lambda PageBolt API
Cold start 10–15 seconds <1 second
Memory per invocation 200–400MB 50MB
Concurrent requests (512MB Lambda) 1–2 10+
Monthly cost (1,000 screenshots) $15–25 (compute) $3–5 (API)
Maintenance Update layers, manage Chromium None
Reliability Crashes, timeouts Guaranteed

For 10,000 screenshots/month: API is 5–10x cheaper than Lambda compute + layer management.

Error Handling & Retries

async function takeScreenshotWithRetry(url, maxRetries = 3) {
  let lastError;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await takeScreenshot(url);
    } catch (error) {
      lastError = error;

      if (error.message.includes('Rate limited')) {
        const delay = (attempt + 1) * 1000;
        await new Promise(resolve => setTimeout(resolve, delay));
      } else {
        throw error;
      }
    }
  }

  throw new Error(`Failed after ${maxRetries} attempts: ${lastError.message}`);
}
Enter fullscreen mode Exit fullscreen mode

Best Practices for Lambda + Screenshot API

  1. Keep payloads small — Only send required fields (url, format, width, height)
  2. Set realistic timeouts — API calls should complete in <5 seconds; set Lambda timeout to 30 seconds
  3. Use async/await — Cleaner than callbacks, better error handling
  4. Monitor with CloudWatch — Log API response times and errors
  5. Scale horizontally — Lambda automatically handles concurrent requests; no need to manage connection pools
  6. Cache screenshots — Store in S3 or ElastiCache to avoid repeated API calls

Key Takeaways

  1. No Chromium binaries — Reduce your deployment size from 200MB to <5MB
  2. Instant cold starts — API calls are fast; no browser launch overhead
  3. Predictable cost — Pay per screenshot, not per CPU second
  4. Zero maintenance — No layer management, no Chromium updates
  5. Better reliability — HTTP calls don't crash like browser processes

Next step: Get your free API key at pagebolt.dev. 100 requests/month, no credit card required. Perfect for testing in Lambda.

Try it free: https://pagebolt.dev/pricing

Top comments (0)