← All projects

AWS Product Scraper: Serverless Data Pipeline

The problem

I needed to collect product data from multiple websites regularly. Running scrapers on my local machine wasn't sustainable. Leaving a server running 24/7 for jobs that run a few times a day seemed wasteful.

This seemed like a perfect use case for serverless. Pay only when the scraper runs, scale automatically when needed, and not worry about server maintenance.

What I built

A fully serverless scraping pipeline on AWS:

  • Lambda functions that scrape target websites
  • EventBridge schedules to trigger scrapes automatically
  • SQS queues to handle distributed processing
  • DynamoDB for storing scraped data
  • S3 for raw files and images

Tech stack

Layer Technology Why
Compute AWS Lambda Pay per execution, auto scaling
Scheduler EventBridge Cron expressions, serverless
Queue SQS Decoupled processing
Database DynamoDB Serverless NoSQL
Storage S3 Raw file storage
Infrastructure AWS CDK TypeScript infra as code

What building this taught me

1. Cold starts are real and they hurt

My first Lambda functions took 3 to 5 seconds to start from cold. For scraping, that added up fast.

I learned to optimize bundle sizes, use provisioned concurrency for critical paths, and accept that some latency is just part of serverless life.

// Before: 3-5 second cold starts
import * as AWS from 'aws-sdk' // Huge bundle

// After: <1 second cold starts
import { DynamoDB } from '@aws-sdk/client-dynamodb' // Tree-shakeable

Resources:

2. DynamoDB pricing is tricky

I assumed "pay per request" meant cheap. Then I saw my first bill.

Scan operations are expensive. Provisioned capacity vs on demand changes costs dramatically. I rewrote queries multiple times to reduce costs.

The lesson: design your access patterns first, then design your table. Not the other way around.

Resources:

3. Infrastructure as code saves your future self

I started clicking around the AWS console. It worked until I needed to replicate the setup for staging.

CDK seemed like overkill initially, but having version controlled infrastructure made everything reproducible and debuggable.

// Define entire scraping infrastructure in TypeScript
const scraperFunction = new lambda.Function(this, 'Scraper', {
  runtime: lambda.Runtime.NODEJS_18_X,
  handler: 'scraper.handler',
  code: lambda.Code.fromAsset('lambda'),
  timeout: Duration.minutes(5),
  memorySize: 1024,
})

// Schedule runs every 6 hours
new events.Rule(this, 'ScraperSchedule', {
  schedule: events.Schedule.rate(Duration.hours(6)),
  targets: [new targets.LambdaFunction(scraperFunction)],
})

Resources:

4. Scrapers break constantly

Websites change their HTML structure without warning. I built monitoring and alerting so I know when scrapers fail, and I designed the system to gracefully handle partial failures without losing data.

// Graceful error handling with partial success
async function scrapeProducts(urls: string[]) {
  const results = await Promise.allSettled(urls.map(url => scrapeOne(url)))

  const successful = results.filter(r => r.status === 'fulfilled')
  const failed = results.filter(r => r.status === 'rejected')

  // Log failures but don't lose successful scrapes
  if (failed.length > 0) {
    await alertSlack(`${failed.length}/${urls.length} scrapes failed`)
  }

  return successful.map(r => r.value)
}

Resources:

5. Memory equals speed equals cost

Lambda pricing ties memory and CPU together. Sometimes allocating more memory actually costs less because the function runs faster.

Sweet spot: 1024 MB costs the same ($0.0025) but runs 6x faster than 128 MB.

I experimented to find the sweet spot for each function.

Resources:

Polite scraping

I built in rate limiting, request delays, and respect for robots.txt. Partly that is the ethical thing to do, and partly it is self-preservation: aggressive scraping gets your IP banned fast.

const rateLimiter = new Bottleneck({
  minTime: 2000, // 2 second delay between requests
  maxConcurrent: 1, // One request at a time per domain
})

// Respect robots.txt
const robotsParser = await fetchRobotsTxt(domain)
if (!robotsParser.isAllowed(url)) {
  console.log(`Skipping ${url} - blocked by robots.txt`)
  return null
}

The bigger realization

Building this scraper taught me that serverless isn't just "functions in the cloud." It's a different way of thinking about architecture. You design around events, embrace eventual consistency, and accept that distributed systems have distributed problems.

What I would do differently

Start with Step Functions for orchestration. I cobbled together Lambda triggers and SQS queues manually. Step Functions would have given me better visibility into the pipeline and easier error handling.

References

Links

Let's build something together.

Got an idea? I'm always up for a new challenge, whether it's a side project, a startup, or something in between.

© 2026 Cyrus David Pastelero. All rights reserved.

Built with Next.js, TypeScript & Tailwind CSS