system design

Low Level Design: Web Scraper Service

What Is a Web Scraper Service?

A Web Scraper Service fetches raw HTML from target URLs, executes JavaScript if needed, extracts structured data, and persists results for downstream consumers. It differs from a general crawler in that it targets specific schemas on known sites rather than discovering the open web. The design must handle rate limiting, dynamic rendering, anti-bot measures, and schema evolution across many target sites simultaneously.

Data Model / Schema

-- Scrape targets configuration
CREATE TABLE scrape_targets (
    target_id    INT PRIMARY KEY AUTO_INCREMENT,
    name         VARCHAR(128) NOT NULL,
    base_url     TEXT NOT NULL,
    extractor_fn VARCHAR(128) NOT NULL,  -- name of parser class/function
    rate_limit   INT DEFAULT 1,          -- requests per second
    js_render    BOOLEAN DEFAULT FALSE,
    active       BOOLEAN DEFAULT TRUE
);

-- Scrape job queue
CREATE TABLE scrape_jobs (
    job_id       BIGINT PRIMARY KEY AUTO_INCREMENT,
    target_id    INT REFERENCES scrape_targets(target_id),
    url          TEXT NOT NULL,
    status       ENUM('queued', 'running', 'done', 'failed'),
    attempt      SMALLINT DEFAULT 0,
    worker_id    VARCHAR(64),
    created_at   TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at   TIMESTAMP
);

-- Extracted structured results
CREATE TABLE scrape_results (
    result_id    BIGINT PRIMARY KEY AUTO_INCREMENT,
    job_id       BIGINT REFERENCES scrape_jobs(job_id),
    target_id    INT,
    url          TEXT,
    payload      JSON,
    schema_ver   SMALLINT,
    scraped_at   TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Core Algorithm and Workflow

The service is organized into three layers:

  • In the fetch layer, workers dequeue jobs from scrape_jobs (or a Kafka topic). Each worker maintains a per-target token bucket for rate limiting. Static pages are fetched via an HTTP client pool. JavaScript-heavy pages are sent to a headless browser pool (Playwright or Puppeteer workers) that return rendered HTML. Responses are streamed to avoid holding large payloads in memory.
  • In the extract layer, raw HTML is passed to the target-specific extractor function identified by extractor_fn. Extractors use CSS selectors or XPath patterns to pull structured fields. Results are validated against a JSON Schema before being accepted. Invalid results trigger a retry with a flag for manual review.
  • In the persist and emit layer, validated payloads are written to scrape_results and also published to a downstream Kafka topic partitioned by target_id. Consumers (data warehouse loaders, API caches) subscribe independently. Deduplication is enforced by hashing the payload and checking for existing records with the same URL and hash within a 24-hour window.

Failure Handling

  • On a network error or timeout, jobs retry with exponential backoff up to a configurable maximum (e.g., 5 attempts). After exhausting retries, jobs move to failed and trigger an alert if the failure rate for a target exceeds a threshold.
  • For anti-bot detection, if a response returns a CAPTCHA page or a known block pattern, the worker marks the job as blocked and pauses all jobs for that target. A human operator or an automatic IP rotation mechanism intervenes before resuming.
  • Extractor functions are versioned to handle schema drift. If the extracted field count drops below an expected minimum, the result is rejected and a schema-drift alert fires, prompting an extractor update. Old results remain queryable via schema_ver.
  • When a worker crashes, jobs assigned to it have a stale updated_at timestamp; a watchdog resets them to queued after a heartbeat timeout.

Scalability Considerations

  • For horizontal scaling, fetch workers are stateless and scale independently. The job queue acts as the buffer. Workers for JS rendering are more expensive; scale those separately based on queue depth for JS-flagged jobs.
  • For proxy rotation, route outbound requests through a proxy pool partitioned by target domain to distribute egress IPs and reduce block rates. Track ban events per proxy-domain pair and evict banned proxies automatically.
  • For extractor isolation, run extractor functions in sandboxed subprocesses to prevent a buggy or malicious extractor from crashing the worker. Use resource limits (CPU, memory, timeout) enforced by the OS.
  • For result storage tiering, keep recent results (last 30 days) in a hot relational store for fast lookup. Archive older results to columnar storage (Parquet on S3) for analytics queries.

See also: Netflix Interview Guide 2026: Streaming Architecture, Recommendation Systems, and Engineering Excellence

See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering

See also: Airbnb Interview Guide 2026: Search Systems, Trust and Safety, and Full-Stack Engineering

newsletter

What's actually being asked right now

Interview patterns & comp trends, straight to your inbox.

No spam. Unsubscribe anytime.

newsletter

What's actually being asked right now

Interview patterns & comp trends, straight to your inbox.

No spam. Unsubscribe anytime.

1972 Soviet postage stamp commemorating the Mars 2 probe

worth a read

Mars For The Rest of Us — a weekly-or-more deep dive on the technical side of Mars exploration: rocket propulsion, microbiology, mission architecture, and everything in between. Written by Maciej Ceglowski.

Read it on Substack →
Scroll to Top