Back to Blog

Scrapy Rotating Proxies: A Practical Setup Guide

Marcus Bennett

Jul 24, 2026 · Guides · 10 min read

TL;DR

Use a rotating gateway for the simplest deployment, a local pool for per-proxy control, and custom middleware for route-specific logic. Keep sticky sessions for stateful workflows, rotate retryable public requests to a new route, and verify the exit IP before increasing concurrency. Test all settings against the target site’s access rules.

Choose a proxy architecture

Rotating proxies are not just an IP-switching feature. Stable crawling depends on how requests are routed, how failed routes are retried, and whether stateful requests keep the same session. Pick the operating model before adding concurrency.

Pattern Rotation control Best fit Tradeoff
Rotating proxy gateway Provider Fast rollout and low maintenance Less application-level control
Scrapy proxy pool Your middleware/package Teams with a maintained proxy list You manage pool health and cooldowns
Custom middleware Your application Route- or workflow-specific rules Highest implementation and test cost

Use a gateway for a single provider endpoint. Use a proxy pool when you already buy or curate individual proxies. Use custom middleware when one spider must treat public pages, login flows, and other stateful workflows differently.

A managed alternative: proxy APIs

Some providers expose a scraping or proxy API instead of a conventional proxy endpoint. This is a managed alternative, not a fourth local rotation architecture: the spider sends a target URL and options to the API, while the provider may manage proxy selection, retries, rendering, or geo settings upstream. It can be useful when the team values a managed request workflow over direct control of Scrapy’s downloader path.

It is not a replacement for every proxy use case. API integrations can change request and response formats, add provider-specific limits, and make direct network troubleshooting less visible. Use a gateway or local pool when you need standard HTTP proxy behavior, direct proxy observability, or fine-grained request routing inside Scrapy.

Method 1: Use a rotating proxy gateway

With a gateway, Scrapy connects to one endpoint and the provider handles exit rotation. Scrapy’s HttpProxyMiddleware documentation supports assigning a proxy through Request.meta["proxy"].

# settings.py
DOWNLOADER_MIDDLEWARES = {
    "scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware": 750,
}

HTTPPROXY_ENABLED = True
PROXY_GATEWAY = "http://user:password@gateway.example.com:9000"
# spider.py
import scrapy
from myproject.settings import PROXY_GATEWAY


class ProductSpider(scrapy.Spider):
    name = "product_spider"
    start_urls = ["https://example.com/products"]

    def start_requests(self):
        for url in self.start_urls:
            yield scrapy.Request(url, callback=self.parse, meta={"proxy": PROXY_GATEWAY})

    def parse(self, response):
        yield {"url": response.url}

This model avoids maintaining exit IPs in the spider. It can also support geo-targeting and sticky sessions when the provider exposes those options. Confirm endpoint syntax, authentication, countries, and session parameters in the provider documentation. For Rola IP, start with the proxy parameter documentation.

Rola IP console example

The gateway still needs crawl controls. Do not respond to every 403 with unlimited retries, and do not use high concurrency before confirming how often one exit is reused.

Rotating proxy gateway configuration parameters

Method 2: Use a Scrapy proxy pool

Choose a local pool when you need to select, pause, and replace individual proxy URLs. scrapy-rotating-proxies is a common package for this purpose. As of July 24, 2026, its PyPI page lists version 0.6.2 with an Alpha development status, so test it against your Scrapy version and downloader stack before production use.

pip install scrapy-rotating-proxies
ROTATING_PROXY_LIST = [
    "http://user:pass@1.2.3.4:8000",
    "http://user:pass@5.6.7.8:8000",
]

DOWNLOADER_MIDDLEWARES = {
    "rotating_proxies.middlewares.RotatingProxyMiddleware": 610,
    "rotating_proxies.middlewares.BanDetectionMiddleware": 620,
}

For a file-backed list, set ROTATING_PROXY_LIST_PATH = "/path/to/proxies.txt".

The package can track proxy health and make concurrency effectively per proxy for proxied requests. It does not supply proxy quality or site-specific ban detection. Requests with request.meta["proxy"] already set are not managed by the package, which is useful for exceptions but important to document in mixed routing setups.

Treat 403, 429, challenge pages, empty templates, and abnormal redirects as separate observations. A small pool plus high concurrency tends to reuse exits too quickly; increasing retries will usually make that worse.

What the middleware actually manages

The package does more than choose a random URL. RotatingProxyMiddleware records whether a route is alive or dead through the _ban request metadata set by BanDetectionMiddleware. A dead route is not discarded forever: it is checked again after randomized exponential backoff. This prevents a temporarily unhealthy route from being selected for every immediate retry while still allowing recovery.

Its retry boundary matters. ROTATING_PROXY_PAGE_RETRY_TIMES defines how many different proxies may be tried before the package treats the result as a page failure rather than a proxy failure. If this is too high, an incorrectly detected block can consume healthy routes. If it is too low, a transient route failure may be misclassified as a target-page problem.

Setting What it controls Operational implication
ROTATING_PROXY_LIST In-memory proxy URLs Best for a small, managed set
ROTATING_PROXY_LIST_PATH File containing proxy URLs Keeps credentials and inventory outside settings code
ROTATING_PROXY_PAGE_RETRY_TIMES Attempts through different routes Set from observed proxy and page failures, not a universal number
ROTATING_PROXY_BACKOFF_BASE Initial cooldown basis Prevents failed routes from returning immediately
ROTATING_PROXY_BACKOFF_CAP Maximum cooldown limit Bounds the time before a route is reconsidered
ROTATING_PROXY_CLOSE_SPIDER Behavior when no live routes remain Useful when continuing without a healthy route would only create noise

Default Scrapy concurrency settings become effectively per proxy for proxied requests when this middleware is enabled. Therefore, CONCURRENT_REQUESTS_PER_DOMAIN = 2 can mean up to two concurrent connections through each active proxy, rather than two requests total. Calculate load from the active pool size, not from one global setting alone.

Customize ban detection for the target response

HTTP status alone is often incomplete. A site can return 200 OK with a challenge page or an empty shell. Define the page signals that represent an unusable response for your authorized use case and test them against saved examples.

from rotating_proxies.policy import BanDetectionPolicy


class StoreBanPolicy(BanDetectionPolicy):
    def response_is_ban(self, request, response):
        text = response.text.lower()
        if response.status in {403, 429}:
            return True
        if "verify you are human" in text or "captcha" in text:
            return True
        if response.status == 200 and "product-grid" not in text:
            return True
        return False
ROTATING_PROXY_BAN_POLICY = "myproject.policies.StoreBanPolicy"

Only add a content marker that is stable for the intended page type. A generic string can mark valid pages as banned and unnecessarily retire good routes. For spiders with several page types, use route-aware conditions or separate spiders instead of one broad keyword rule.

Scrapy proxy pool retry log

Method 3: Build custom proxy middleware

Custom downloader middleware is appropriate when routing depends on the URL, page type, or session. Scrapy lets middleware modify or reschedule a request in process_request, process_response, and process_exception; see the downloader middleware guide.

import random


class CustomProxyMiddleware:
    def __init__(self, proxies, max_retries=3):
        self.proxies = proxies
        self.max_retries = max_retries

    @classmethod
    def from_crawler(cls, crawler):
        return cls(crawler.settings.getlist("CUSTOM_PROXY_LIST"),
                   crawler.settings.getint("CUSTOM_PROXY_RETRY_TIMES", 3))

    def pick_proxy(self, excluded=None):
        candidates = [item for item in self.proxies if item != excluded] or self.proxies
        if not candidates:
            raise RuntimeError("CUSTOM_PROXY_LIST is empty")
        return random.choice(candidates)

    def process_request(self, request, spider):
        if "proxy" not in request.meta:
            request.meta["proxy"] = self.pick_proxy()

    def retry_with_new_proxy(self, request, spider):
        retries = request.meta.get("proxy_retry_times", 0) + 1
        if retries > self.max_retries:
            return None
        retry = request.copy()
        retry.dont_filter = True
        retry.meta["proxy_retry_times"] = retries
        retry.meta["proxy"] = self.pick_proxy(request.meta.get("proxy"))
        return retry

    def is_retryable_response(self, response):
        if response.status == 407:
            return False  # Fix proxy credentials or allowlisting before retrying.
        return response.status in {403, 429} or "captcha" in response.text.lower()

    def process_response(self, request, response, spider):
        if self.is_retryable_response(response):
            return self.retry_with_new_proxy(request, spider) or response
        return response

    def process_exception(self, request, exception, spider):
        return self.retry_with_new_proxy(request, spider)

This is a minimal example, not a universal retry policy. It deliberately does not retry 407 responses because those usually require a credential, endpoint, or allowlist correction. Production middleware should add cooldowns, structured logs, and separate handling for network failures, authentication failures, and rejected pages. It can bind a proxy to a cookiejar for a workflow, route login traffic differently from public pages, and record the proxy identifier and retry reason for later review.

Enable this middleware in the project settings. Do not enable it alongside a proxy-pool middleware for the same requests unless you have defined and tested which component owns proxy assignment and retries.

# settings.py
DOWNLOADER_MIDDLEWARES = {
    "myproject.middlewares.CustomProxyMiddleware": 610,
}

Route requests by workflow, not only by domain

One spider can legitimately need more than one proxy behavior. The following policy is a useful starting point, subject to the site’s terms and the provider’s session rules.

Request type Proxy behavior Cookie/session behavior Failure response
Public listing or search page Rotate across available exits Isolated or short-lived jar Retry once through a different route after classification
Product or detail page Rotate at a measured rate Keep the jar only if navigation depends on it Check for challenge and empty-template responses
Login or multi-step form Sticky route Bind one proxy identity to one cookiejar End or restart the authorized workflow; do not rotate mid-flow by default
Cursor-based pagination Usually sticky for the sequence Preserve cookies and cursor state Retry with care because a changed route can invalidate state
Provider health or IP-check request Explicit diagnostic route No business cookies Record exit, country, and latency separately

For a sticky workflow, save the selected proxy in request metadata and use the same cookiejar ID for every request in that sequence. Release both when the sequence ends. This makes a session failure explainable: operators can see whether it was caused by credentials, the exit route, the application session, or the target response.

Custom Scrapy proxy retry and exception log

Configure proxy authentication correctly

Most 407 errors and early connection failures are endpoint or credential-format problems. Use the authentication model documented by the provider.

proxy = "http://user:password@ip:port"
request.meta["proxy"] = proxy

For providers that explicitly require Basic Auth in a header:

import base64

credentials = base64.b64encode(b"user:password").decode("ascii")
yield scrapy.Request(
    url,
    callback=self.parse,
    meta={"proxy": "http://ip:port"},
    headers={"Proxy-Authorization": f"Basic {credentials}"},
)

Check the scheme, port, special-character encoding, and whether the provider uses username/password or IP allowlisting. Do not send both URL credentials and a Proxy-Authorization header unless the provider supports that combination. Rola IP documents protocol support, proxy credentials, and whitelist access.

Before debugging a spider, make one request with the exact endpoint and authentication method. A failure at this stage is an integration problem, not a rotation problem. Also avoid writing full proxy URLs or credentials to logs; log a provider-defined identifier or a redacted host and port instead.

Make rotation stable

Use a new route for retryable failures

On a retryable failure, prefer a different exit. Keep retry ceilings low and record the reason: 403, 429, timeout, DNS or TLS error, challenge page, or unexpected redirect. Cool down an unhealthy proxy instead of immediately returning it to active rotation.

Do not layer multiple retry systems without deciding which one owns each event. For example, Scrapy’s RetryMiddleware, a rotating-proxies retry, and custom process_response logic can otherwise reschedule the same request several times. Define one owner for proxy-route retries and reserve the general retry mechanism for application-level transient errors.

Use sticky sessions for stateful workflows

Login flows, carts, cursor pagination, and multi-step forms normally need one proxy identity and one cookiejar until the workflow ends. Public listing pages can rotate more freely. Review the provider’s sessionid and sessiontime parameters before implementing sticky routing.

Size concurrency with the exit pool

Start at low concurrency, then adjust CONCURRENT_REQUESTS, DOWNLOAD_DELAY, DOWNLOAD_TIMEOUT, retry limits, and site-level limits from observed results. A planning estimate is:

minimum exits ≈ total requests per minute / requests per minute one IP can safely carry

The safe per-IP rate and buffer for retries must come from authorized testing, proxy quality, and the target site’s rules. They are not universal thresholds.

Verify the setup before scaling

  1. Check that requests use the expected exit IP:

    scrapy shell "https://httpbin.org/ip"
    

    Repeat the request to distinguish a rotating gateway from a static endpoint. If exit country or rotation looks wrong, consult Rola IP’s IP attribution and switching guide.

  2. Inspect logs to confirm the proxy identifier, retry count, failure class, and whether a retry selected a new route.

  3. Run a small, authorized load test. Track 403 and 429 ratios, timeouts, average response time, per-proxy reuse, and session failures. Scale only after those metrics remain stable.

Minimum logging fields

Logs should make a failed request reconstructable without exposing credentials. Include a request ID, target host or route class, redacted proxy ID, cookiejar ID where relevant, retry count, response status, failure class, elapsed time, and the selected next action. This distinguishes a rejected page from an authentication error or a dead route.

Useful counters include live versus cooling-down proxies, retries by cause, unexpected-page ratio, and requests completed per route class. Alert on changes from the crawl’s own baseline. A fixed percentage threshold can be misleading when sites, geographies, and traffic patterns differ.

Troubleshooting

Connection timeout or refused connection

Temporarily cool down the proxy and check whether the route recovers. Avoid unlimited retries. Set alert thresholds from historical tests rather than fixed industry percentages.

meta["proxy"] is set, but traffic is direct

Check middleware order and search for later middleware that overwrites request.meta["proxy"]. This is common when retry, user-agent, and custom routing middleware all touch the request.

Repeated 403 or 429 responses

Changing IPs alone will not solve pacing, cookie, permissions, or browser-challenge problems. Review request frequency, session continuity, response validation, and the site’s allowed access methods. Use an available API instead of attempting to work around access controls.

It works locally but fails under load

Batch jobs increase exit reuse and replayed failures. Reproduce the production shape with a small controlled test before raising throughput.

Also compare production configuration with local configuration: environment variables, allowlisted source IP, endpoint region, middleware order, proxy pool size, and enabled retry middleware. A valid local test does not prove that those settings are identical in a deployment.

Conclusion

Choose a rotating gateway for low-maintenance deployment, a proxy pool for inventory-level control, or custom middleware for workflow-specific routing. Then verify exit selection, retry routing, and session behavior before increasing concurrency.

Rola IP can reduce gateway and exit-pool maintenance while offering geo and session options. Confirm current product parameters in the pricing and product page or test from the registration and login portal.

Compliance note: Use proxy rotation only for your own systems, authorized testing, or data access that follows site rules, robots rules, terms of service, and applicable law. Do not use it to circumvent access controls or account restrictions.

Frequently Asked Questions

Ready to start collecting data at scale?

Try for Free