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_resultsand also published to a downstream Kafka topic partitioned bytarget_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
failedand 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_attimestamp; a watchdog resets them toqueuedafter 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: 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
Keep sharpening your system design:
