Building Resilient Web Scrapers: Retry, Circuit Breaker, and Fallback Patterns
Best Practices

Building Resilient Web Scrapers: Retry, Circuit Breaker, and Fallback Patterns

Learn how to add retry logic, circuit breakers, and fallbacks to your scraping pipelines to handle transient failures and maintain data collection reliability.

H
Herald Blog Service
3 min read
4 views

AlterLab handles this automaticallyscrape any URL with one API call. No infrastructure required.

Try it free

TL;DR

Add retry logic with exponential backoff and jitter, implement a circuit breaker to halt requests after repeated failures, and define fallbacks like cached data or alternative endpoints. These patterns keep your scraping pipeline reliable despite transient network issues, rate limits, or temporary blocks.

Why Resilience Matters in Scraping

Web scraping pipelines encounter intermittent failures: network timeouts, HTTP 5xx responses, rate limits, or temporary anti-bot challenges. Without handling these, a single hiccup can halt an entire data collection job, leading to stale datasets and wasted compute. Resilience patterns isolate failures, allow automatic recovery, and preserve throughput.

Retry Strategies: Exponential Backoff with Jitter

A basic retry simply repeats the request immediately, which can worsen load on a struggling server. Instead, use exponential backoff: wait 1s, then 2s, then 4s, etc., up to a maximum delay. Adding jitter (random delay) prevents thundering herd problems when many clients recover simultaneously.

Python SDK Example

Python
import time
import random
import alterlab

client = alterlab.Client("YOUR_API_KEY")

def scrape_with_retry(url, max_attempts=5):
    attempt = 0
    while attempt < max_attempts:
        try:
            response = client.scrape(url)
            if response.status_code < 500:
                return response
        except alterlab.APIError as exc:
            if exc.status_code >= 500:
                pass  # treat as retryable
            else:
                raise  # client errors (4xx) are not retried
        attempt += 1
        delay = (2 ** attempt) + random.uniform(0, 1)  # exponential backoff + jitter
        time.sleep(delay)
    raise RuntimeError(f"Failed to scrape {url} after {max_attempts} attempts")

# usage
data = scrape_with_retry("https://example.com/public-data")
print(data.json)

cURL Example with Retry Header

Bash
curl -X POST https://api.alterlab.io/v1/scrape \
  -H "X-API-Key: YOUR_KEY" \
  -H "Retry-After: 1" \
  -d '{"url": "https://example.com/public-data", "max_retries": 4}'

Note: The AlterLab API honors the Retry-After header and respects the max_retries parameter, applying exponential backoff internally when enabled.

Circuit Breaker Pattern

A circuit breaker tracks consecutive failures. After a threshold (e.g., five failures), it trips and blocks further requests for a cooldown period (e.g., 30 seconds). During the cooldown, calls fail fast, saving resources and allowing the remote service to recover. After the cooldown, a limited trial request tests if the service is healthy.

Implementation Sketch

Python
import time
from enum import Enum

class State(Enum):
    CLOSED = 0
    OPEN = 1
    HALF_OPEN = 2

class CircuitBreaker:
    def __init__(self, fail_threshold=5, timeout=30):
        self.fail_threshold = fail_threshold
        self.timeout = timeout
        self.fail_count = 0
        self.state = State.CLOSED
        self.last_failure_time = None

    def call(self, func, *args, **kwargs):
        if self.state == State.OPEN:
            if time.time() - self.last_failure_time > self.timeout:
                self.state = State.HALF_OPEN
            else:
                raise RuntimeError("Circuit breaker is OPEN")
        try:
            result = func(*args, **kwargs)
            self.on_success()
            return result
        except Exception as exc:
            self.on_failure()
            raise exc

    def on_success(self):
        self.fail_count = 0
        self.state = State.CLOSED

    def on_failure(self):
        self.fail_count += 1
        self.last_failure_time = time.time()
        if self.fail_count >= self.fail_threshold:
            self.state = State.OPEN

Integrate the breaker around your scrape call:

Python
breaker = CircuitBreaker(fail_threshold=4, timeout=20)

def safe_scrape(url):
    return breaker.call(scrape_with_retry, url)

# usage
try:
    data = safe_scrape("https://example.com/public-data")
except RuntimeError as err:
    print("Scrape unavailable:", err)

Fallbacks: Cached Data and Alternative Endpoints

When the circuit breaker is open or retries exhaust, serve stale data from a cache or query a mirror site. This ensures downstream processes continue receiving data, albeit possibly less fresh.

Simple Cache Fallback

Python
import json
import os
from datetime import datetime, timedelta

CACHE_DIR = "./scrape_cache"
CACHE_TTL = timedelta(hours=6)

def get_cached(url):
    path = os.path.join(CACHE_DIR, hash(url) + ".json")
    if not os.path.exists(path):
        return None
    with open(path) as f:
        entry = json.load(f)
    if datetime.fromisoentry["fetched"] + CACHE_TTL < datetime.utcnow():
        return None
    return entry["data"]

def save_cache(url, data):
    os.m
Share

Was this article helpful?

Frequently Asked Questions

A circuit breaker stops requests after a threshold of failures, allowing the service to recover before retrying.
Retry strategies with exponential backoff and jitter reduce the chance of repeated failures by spacing out attempts.
Use a fallback when primary sources fail repeatedly, such as switching to cached data or an alternative endpoint.