Skip to main content
[ DOCS / SCRAPY ]

Scrapy proxy middleware for rotating residential IPs

Put Ludax behind every Scrapy request with one downloader middleware: a new home IP per request or one IP per account, the country you pick, and a crawl that stops by itself when a login is refused instead of retrying it into a block. Or, for a quick test, put the proxy in meta and change nothing else.

  • One middlewareevery request, no spider changes
  • Rotating or stickyper request, per spider or per account
  • Browser TLS at the gateway139 fingerprints, no extra library

Scrapy only downloads HTML, so a GB goes a long way: start with 1 GB for $0.99. Tested with Scrapy 2.20 and Python 3.14 — November 2026

KEY FACTS

  • Scrapy takes a proxy per request in meta["proxy"], for example http://resi-lx_k3m9q2p7xa:[email protected]:7777; its HttpProxyMiddleware turns the username and password into the login header.
  • Scrapy keeps connections open, and one connection is one IP: requests that reuse a connection share its IP. For a new IP on every request give each request its own session token; the middleware below does it for you.
  • For the same IP across a flow add -session-<token>-time-<minutes> to the password (up to 1000 minutes). Without -time- a session lasts 1 minute.
  • A refused login shows as TunnelError: Could not open CONNECT tunnel with proxy ... [{'status': 407, ...}]. Scrapy’s RetryMiddleware retries it twice by default, and 30 failed logins in 5 minutes block your IP for 10 minutes: the middleware stops the crawl at the first 407 instead.
  • Recent Scrapy starts a spider with async def start(). Tested with Scrapy 2.20 on Python 3.14.
  • Scrapy’s TLS handshake is not a browser’s. The Residential TLS port (gw.ludaxproxy.com:9999, -AE-chrome154 in the username) connects to the site with a real browser handshake; Scrapy needs no certificate setup for it.

Using an AI assistant? Give it this guide

One Markdown file with everything ChatGPT, Claude, Cursor or Copilot needs to write working Ludax code for Scrapy: the connection format, the gateways in force, sessions, errors and tested examples. Copy it into the chat, or paste this prompt:

Read https://ludaxproxy.com/docs/scrapy.md and add Ludax proxies to my Scrapy project.

[ 01 / ONE SPIDER ]

Test a Ludax proxy with one spider

If the log shows a scraped item with a residential IP, your Scrapy proxy works. This is the quickest test; for a real project use the middleware.

  1. Find your proxy URL parts

    On the dashboard’s Generate page: the username lx_k3m9q2p7xa, written resi-lx_k3m9q2p7xa for Residential, premi- for Residential Premium, and the plan password. Options such as the country go after the password (all options).

  2. Install Scrapy

    install.sh
    pip install scrapy
  3. Run this spider

    whoami_spider.py
    import scrapy
    
    PROXY = "http://resi-lx_k3m9q2p7xa:[email protected]:7777"
    
    
    class WhoamiSpider(scrapy.Spider):
        name = "whoami"
    
        async def start(self):
            # the proxy goes in meta; Scrapy sends the login for you
            yield scrapy.Request("https://api.ludaxproxy.com/whoami", meta={"proxy": PROXY})
    
        def parse(self, response):
            yield response.json()
    run.sh
    scrapy runspider whoami_spider.py

    Among the log lines you should see the scraped item, with your own exit IP:

    output
    DEBUG: Scraped from <200 https://api.ludaxproxy.com/whoami>
    {'ip': '203.0.113.24', 'country': 'United States', 'type': 'residential', 'tls_profile': None, 'latency_ms': 412}

    That is a real home connection in the United States, picked by -country-us in the password. tls_profile is None because the site sees Scrapy’s own handshake (why that matters). Codes for every country are in the locations directory.

    Official docs: Scrapy: HttpProxyMiddleware

[ 02 / THE MIDDLEWARE ]

A Scrapy proxy middleware for Ludax

One file and five settings, and every request in the project goes through Ludax. Your spiders stay as they are.

  1. A new IP on every request (LUDAX_STICKY_MINUTES = 0), or one IP for the whole spider (30), or one per account with meta["ludax_session"].
  2. The country for every request in one setting.
  3. Stops at the first refused login. Without it Scrapy retries a 407 and every queued request tries again, which is how a typo turns into a 10-minute IP block.
  4. A proxy in meta still wins, for the odd request that needs another country.
middlewares.py
import secrets

from scrapy.core.downloader.handlers.http11 import TunnelError
from scrapy.exceptions import IgnoreRequest
from scrapy.utils.defer import deferred_from_coro


class LudaxProxyMiddleware:
    """Sends every request through Ludax and stops the crawl on a refused login."""

    def __init__(self, crawler):
        s = crawler.settings
        self.crawler = crawler
        self.user = s["LUDAX_USER"]
        self.password = s["LUDAX_PASSWORD"]
        self.gateway = s.get("LUDAX_GATEWAY", "gw.ludaxproxy.com:7777")
        self.country = s.get("LUDAX_COUNTRY")
        self.sticky_minutes = s.getint("LUDAX_STICKY_MINUTES")  # 0: a new IP per request
        self.tls = "-AE-" in self.user or "-NAE-" in self.user  # a TLS port
        self.spider_token = secrets.token_hex(4)

    @classmethod
    def from_crawler(cls, crawler):
        return cls(crawler)

    def process_request(self, request):
        if "proxy" in request.meta:
            return None  # the spider chose a proxy itself
        if self.sticky_minutes:
            # one IP for the whole spider; meta["ludax_session"] gives a flow its own IP
            token = request.meta.get("ludax_session", self.spider_token)
            options = f"-session-{token}-time-{self.sticky_minutes}"
            if self.tls:
                options += "-sameip"  # on the TLS ports, hold the IP as well as the browser
        else:
            # a token of its own: a new connection, so a new IP, for every request
            options = f"-session-{secrets.token_hex(4)}-time-1"
        if self.country:
            options += f"-country-{self.country}"
        request.meta["proxy"] = f"http://{self.user}:{self.password}{options}@{self.gateway}"
        return None

    def refused(self, detail):
        # every other request would be refused the same way, and retries only
        # count towards the block (30 failed logins in 5 minutes)
        deferred_from_coro(self.crawler.engine.close_spider_async(reason="proxy_login_refused"))
        raise IgnoreRequest(f"Ludax refused the login (407): {detail}")

    def process_response(self, request, response):
        if response.status == 407:  # http:// URLs get the gateway's answer as a response
            self.refused(request.url)
        return response

    def process_exception(self, request, exception):
        if isinstance(exception, TunnelError) and "'status': 407" in str(exception):
            self.refused(exception)  # https:// URLs get it as a tunnel error
        return None
settings.py
import os

DOWNLOADER_MIDDLEWARES = {
    # before HttpProxyMiddleware (750), which turns meta["proxy"] into the login header
    "myproject.middlewares.LudaxProxyMiddleware": 610,
}

LUDAX_USER = "resi-lx_k3m9q2p7xa"
LUDAX_PASSWORD = os.environ["LUDAX_PASSWORD"]  # the plan password, kept out of the code
LUDAX_GATEWAY = "gw.ludaxproxy.com:7777"
LUDAX_COUNTRY = "us"  # None: any country
LUDAX_STICKY_MINUTES = 0  # 0: a new IP per request; 30: one IP for 30 minutes
run_crawl.sh
export LUDAX_PASSWORD="YOUR_PASSWORD"
scrapy crawl myspider

Replace myproject with your project’s package name. Priority 610 places it before Scrapy’s HttpProxyMiddleware (750), which turns the proxy URL into the login header, and lets it see errors before the RetryMiddleware (550) does. Tested: with a wrong password and 20 queued requests the crawl closed with finish_reason: proxy_login_refused after 4 attempts, one per concurrent request.

Official docs: Scrapy: downloader middleware · Scrapy: settings

[ 03 / SETTINGS GENERATOR ]

Scrapy proxy settings generator

Choose the product, the country and whether the IP should stay the same, and copy the settings for the middleware above, or a spider with the proxy in meta for a quick test. Type your own username and password to get code you can run as is: they stay in this browser tab and are never sent anywhere.

Product

Real home IPs for everyday scraping. Gateway: gw.ludaxproxy.com:7777

IP address

YOUR CONNECTION STRING

http://resi-lx_k3m9q2p7xa:YOUR_PASSWORD-country-us@gw.ludaxproxy.com:7777
  • username (+ browser on TLS)
  • plan password
  • session
  • location
  • gateway
settings.py
[ 04 / SPIDERS ]

Spiders for sessions, geo and speed

Each spider runs with the middleware above, unless it says otherwise.

01

One IP per account: sticky sessions in Scrapy

Logged-in scraping, carts, anything a site expects from one visitor. Each account gets its own token and so its own IP, held for as long as LUDAX_STICKY_MINUTES says.

accounts_spider.py
import scrapy


class AccountsSpider(scrapy.Spider):
    """Each account keeps its own IP: run with LUDAX_STICKY_MINUTES = 30."""

    name = "accounts"

    async def start(self):
        for account in ["shop1", "shop2", "shop3"]:
            # the same ludax_session = the same IP; letters and digits only
            for page in range(3):
                yield scrapy.Request(
                    f"https://api.ludaxproxy.com/whoami?account={account}&page={page}",
                    meta={"ludax_session": account},
                    cb_kwargs={"account": account},
                    dont_filter=True,
                )

    def parse(self, response, account):
        yield {"account": account, "ip": response.json()["ip"]}
output
{'account': 'shop1', 'ip': '203.0.113.24'}
{'account': 'shop1', 'ip': '203.0.113.24'}
{'account': 'shop2', 'ip': '198.51.100.7'}
...

A session token is 3 to 24 letters or digits, so shop1 works and shop-1 does not. Sessions hold up to 1000 minutes on Residential and Residential Premium.

02

Geo-targeting per request: one page, several countries

Local prices and search results. The location goes in the password; a proxy in meta overrides the middleware for that request.

prices_spider.py
import scrapy

USER = "resi-lx_k3m9q2p7xa"
PASSWORD = "YOUR_PASSWORD"
GATEWAY = "gw.ludaxproxy.com:7777"


class PricesSpider(scrapy.Spider):
    """The same page seen from three countries."""

    name = "prices"

    async def start(self):
        for country in ["us", "de", "jp"]:
            proxy = f"http://{USER}:{PASSWORD}-country-{country}@{GATEWAY}"
            yield scrapy.Request(
                "https://api.ludaxproxy.com/whoami",
                meta={"proxy": proxy},  # a proxy in meta wins over the middleware
                cb_kwargs={"country": country},
                dont_filter=True,
            )

    def parse(self, response, country):
        yield {"asked": country, "got": response.json()["country"]}
output
{'asked': 'us', 'got': 'United States'}
{'asked': 'de', 'got': 'Germany'}
{'asked': 'jp', 'got': 'Japan'}

Regions and cities follow the country: -country-de-state-by-city-munich. Codes for every country are in the locations directory.

03

Fast and polite: concurrency, AutoThrottle and retries

Big crawls. Scrapy already retries a dropped exit (502); these settings keep the pace sensible so the site does not start blocking you.

settings_speed.py
# settings.py: fast, but gentle on the site and on your GB
CONCURRENT_REQUESTS = 16
CONCURRENT_REQUESTS_PER_DOMAIN = 8
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_TARGET_CONCURRENCY = 4
DOWNLOAD_TIMEOUT = 30

# a dropped exit (502) is retried by Scrapy's own RetryMiddleware: 502 is in its
# default RETRY_HTTP_CODES, and a failed tunnel is retried as well
RETRY_TIMES = 3

Plans are priced per GB, not per request: Scrapy downloads only the HTML, so it goes much further per GB than a browser.

[ 05 / GETTING BLOCKED? ]

Scrapy blocked with residential proxies? Fix the TLS fingerprint

If a site blocks your spider even from clean home IPs, it is usually not the IP. It is the TLS handshake.

Before a site reads your URL or a single header, your client says hello: the TLS handshake. Chrome says hello one way; Scrapy (Twisted and OpenSSL) says it in a way no browser does. Anti-bot systems read that hello as a fingerprint (JA3/JA4) and block on it, however good the IP is.

The usual fix is a headless browser plugin and a much slower crawl. With Residential TLS proxies you change two settings instead: the gateway connects to the site with a real browser’s handshake, HTTP/2 settings and matching headers, and your spiders stay plain Scrapy.

settings_tls.py
# settings.py: the Residential TLS port, looking like Chrome 154
LUDAX_USER = "resi-lx_k3m9q2p7xa-AE-chrome154"
LUDAX_GATEWAY = "gw.ludaxproxy.com:9999"
  1. The TLS gateway: gw.ludaxproxy.com:9999 (Residential Premium TLS: gw.ludaxproxy.com:8081).
  2. -AE-chrome154 at the end of the username: the browser to look like. AE also sets a matching User-Agent for you.
  3. No certificate setup in Scrapy: the gateway re-signs HTTPS with the Ludax CA, and Scrapy does not check certificates by default. If your project does, add ludax-ca.crt to its trust store.

Check it with the quickstart spider on the TLS port: tls_profile changes from None to the browser you picked. With LUDAX_STICKY_MINUTES the middleware adds -sameip on the TLS ports, so the IP is held as well as the browser.

Which plan for a Scrapy crawl?

Start on Residential. If a site blocks you from clean IPs, switch the same spiders to Residential TLS with the two settings above: 1 GB is enough to test a target properly, and your username stays the same.

Get 1 GB of TLS — $1.49

Good to know: AE replaces your User-Agent; with NAE your headers pass through unchanged, so send a User-Agent from the same browser. Pages that build their content with JavaScript still need a browser: see the Playwright guide. Request and response bodies are never stored.

[ 06 / LOG LINES ]

Scrapy log lines and what they mean

The exact line Scrapy logs, what it means and the one thing to do. All gateway answers: status codes.

SYMPTOM, CAUSE, FIX
You seeWhyFix
TunnelError: Could not open CONNECT tunnel with proxy gw.ludaxproxy.com:7777 [{'status': 407, 'reason': b'Proxy Authentication Required'}]The login was refused: a typo, a prefix that does not match the port, or a plan with no data left.Copy the credentials again from Generate; check the plan. Every 407 cause.
Gave up retrying <GET ...> (failed 3 times): Could not open CONNECT tunnel ... 407Scrapy’s RetryMiddleware retried a refused login, for every queued request: 30 failed logins in 5 minutes block your IP for 10 minutes.Use the middleware: it stops the crawl at the first 407.
Ignoring response <407 http://...>: HTTP status code is not handled or not allowedThe same refused login, on an http:// URL.As above.
Could not open CONNECT tunnel ... [{'status': 502, 'reason': b'Bad Gateway'}]The exit IP dropped or could not reach the site.Nothing to do: Scrapy retries it (recipe 03). Many at once: check the network status.
The spider closes at once with Crawled 0 pagesRecent Scrapy starts a spider with async def start(); in our test with Scrapy 2.20 a spider with only start_requests() sent nothing.Rename it to async def start(self): and keep the yields.
The same IP on many requests, with no sessionScrapy reuses open connections, and one connection is one IP.The middleware with LUDAX_STICKY_MINUTES = 0 gives each request its own token, so its own connection and IP.
The IP changes in the middle of a flowNo -time- (a session then lasts 1 minute), the time ran out, or no -sameip on a TLS port.Set LUDAX_STICKY_MINUTES and use meta["ludax_session"] per flow (recipe 01).
TCP connection timed out or Connection was refused to the gatewayA wrong host or port, or a firewall that blocks outgoing connections.Check LUDAX_GATEWAY: gw.ludaxproxy.com with 7777, 9999, 8080 or 8081.
[ 07 / FAQ ]

Scrapy proxy questions

Short answers. The full option list is in proxy authentication.

01How do I use a proxy with a username and password in Scrapy?

Put the full proxy URL in the request's meta: meta={"proxy": "http://resi-lx_k3m9q2p7xa:[email protected]:7777"}. Scrapy's built-in HttpProxyMiddleware sends the username and password as the proxy login. For a whole project, set it in a downloader middleware instead.

02How do I rotate proxies in Scrapy?

Every new connection leaves from a new IP, but Scrapy reuses open connections. Give each request its own session token in the password (-session-<random>-time-1) and it gets its own connection and so its own IP. The LudaxProxyMiddleware on this page does it for every request.

03Do I need scrapy-rotating-proxies or a proxy list?

No. Ludax is one gateway: the rotation happens behind it, from a pool of residential IPs. One proxy URL replaces the whole list.

04How do I keep the same IP for a login in Scrapy?

Use one session token for every request of the flow: -session-<token>-time-<minutes> in the password, up to 1000 minutes. With the middleware, set LUDAX_STICKY_MINUTES and pass meta={"ludax_session": "account1"} on the flow's requests.

05Why does Scrapy retry a 407 Proxy Authentication Required?

Scrapy's RetryMiddleware treats the failed CONNECT tunnel like a network error and retries it. A 407 never fixes itself, and 30 failed logins in 5 minutes block your IP for 10 minutes, so the middleware on this page stops the crawl at the first 407.

06Why is my spider blocked even with residential proxies?

Usually the TLS handshake: Scrapy does not say hello like a browser, and anti-bot systems block on that fingerprint. The Residential TLS port (gw.ludaxproxy.com:9999 with -AE-chrome154 in the username) connects with a real browser handshake; Scrapy needs no other change.

07What is the cheapest way to run a Scrapy crawl through Ludax?

Start with 1 GB of Residential for $0.99, prepaid and with no subscription; switch to Residential TLS ($1.49 for 1 GB) only if sites block the handshake. There is no free trial.

[ START NOW ]

Send your next crawl through home IPs

  1. Buy 1 GB of Residential.
  2. Drop in the middleware from section 02.
  3. Generate your settings with the settings generator and crawl.

Stuck? Send us your settings and the log on Telegram and we debug it with you. Every plan and price: pricing.