Skip to content
LightningBytes
Back to Blog

Configuring Scrapy Proxy Middleware

Setting up Scrapy with a proxy, writing a rotating middleware with health tracking, and tuning throttle and retry settings so the spider backs off cleanly.

by LightningBytes Team
  • web-scraping
  • tutorials

Scrapy is a framework rather than a script, which means it already has the concepts a resilient scraper needs: middleware, throttling, retry policy and concurrency limits. Wiring a proxy into those correctly gives you behaviour that individual scripts recreate badly.

The simplest option

Scrapy ships with an HTTP proxy middleware. Enable it and set the meta key per request.

# settings.py
DOWNLOADER_MIDDLEWARES = {
    "scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware": 110,
}
yield scrapy.Request(url, meta={"proxy": "http://user:pass@host:port"})

That is enough for a fixed endpoint. It does nothing useful for rotation, which is what most projects actually need.

A rotating middleware

A custom middleware picks an endpoint per request and records the outcome so failing endpoints are avoided.

import random

class RotatingProxyMiddleware:
    def __init__(self, endpoints, max_failures=3, cooldown=60):
        self.endpoints = [
            {"url": url, "failures": 0, "cooldown_until": 0.0} for url in endpoints
        ]
        self.max_failures = max_failures
        self.cooldown = cooldown

    @classmethod
    def from_crawler(cls, crawler):
        return cls(crawler.settings.getlist("PROXY_ENDPOINTS"))

    def _pick(self):
        import time
        now = time.monotonic()
        available = [e for e in self.endpoints if e["cooldown_until"] <= now]
        return random.choice(available or self.endpoints)

    def process_request(self, request, spider):
        endpoint = self._pick()
        request.meta["proxy"] = endpoint["url"]
        request.meta["proxy_endpoint"] = endpoint

    def process_response(self, request, response, spider):
        endpoint = request.meta.get("proxy_endpoint")
        if endpoint is None:
            return response
        if response.status in (403, 429):
            self._penalise(endpoint)
        else:
            endpoint["failures"] = 0
        return response

    def process_exception(self, request, exception, spider):
        endpoint = request.meta.get("proxy_endpoint")
        if endpoint is not None:
            self._penalise(endpoint)

    def _penalise(self, endpoint):
        import time
        endpoint["failures"] += 1
        if endpoint["failures"] >= self.max_failures:
            endpoint["cooldown_until"] = time.monotonic() + self.cooldown
            endpoint["failures"] = 0

Register it and provide the endpoints:

DOWNLOADER_MIDDLEWARES = {
    "myproject.middlewares.RotatingProxyMiddleware": 110,
}

PROXY_ENDPOINTS = [
    "http://user:pass@host:port-session-a",
    "http://user:pass@host:port-session-b",
]

Notice that each endpoint carries its own session value, which is how stickiness is expressed. The mechanics are in Understanding Proxy Session IDs.

Separating transport failures from target rejections

The middleware above penalises both connection errors and 403/429 responses. In production these deserve different handling.

A connection error is an endpoint problem. A 403 is the target reacting to your request pattern or the address's reputation, and a 429 is an explicit rate limit that should be honoured rather than rotated around.

def process_response(self, request, response, spider):
    if response.status == 429:
        retry_after = response.headers.get("Retry-After")
        # Back off globally rather than blaming one endpoint.
        spider.crawler.engine.pause()
        # ... schedule resume
    return response

The distinction is the one we draw in Rate Limiting vs Blocking, and getting it wrong sends you rotating away from working endpoints for no reason.

Throttling settings that matter

Scrapy's defaults are polite. Tighten or loosen them deliberately rather than turning them off.

AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 1.0
AUTOTHROTTLE_MAX_DELAY = 30.0
AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0   # per domain

DOWNLOAD_DELAY = 0.5
RANDOMIZE_DOWNLOAD_DELAY = True          # jitter is not optional

AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0 is the important correction. Scrapy's default target is higher, but for defended targets one request at a time per domain with real pacing is more likely to succeed than four in parallel. The reasoning is in How Anti-Bot Systems Detect Scrapers.

Retry configuration

Scrapy retries certain status codes by default. Tune the list and cap the attempts.

RETRY_ENABLED = True
RETRY_TIMES = 2
RETRY_HTTP_CODES = [429, 500, 502, 503, 504, 522, 524]

Keep RETRY_TIMES low. Unbounded retries are a common way to escalate a soft limit into a hard block, and the middleware should move to a fresh endpoint rather than hammering one.

Logging the exit IP

Attribution is what turns a mystery into a fix. Log the endpoint or session with each response so a failure can be traced to an address.

def process_response(self, request, response, spider):
    spider.logger.debug(
        "response url=%s status=%s session=%s",
        response.url,
        response.status,
        request.meta.get("proxy_endpoint", {}).get("url"),
    )
    ...

Verify an endpoint independently with the proxy checker, which reports the exit IP and added latency, and confirm location with IP lookup if you are using targeting.

Where to go next

With middleware in place, the remaining work is parsing accuracy and health monitoring. Parsing HTML and JSON Reliably covers validation, and Monitoring Scraper Health covers the metrics that tell you the spider is degrading before it fails. For the general proxy layer, What Is a Proxy Pool describes the design this middleware implements.

Start working with cleaner IPs

Clean, pre-filtered residential and mobile proxies, sign up and send your first request in minutes.

We use cookies for authentication and security. With your consent we also enable optional marketing & analytics cookies. See our privacy policy.