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.
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.
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).
Install Scrapy
install.sh
pip install scrapy
Run this spider
whoami_spider.py
import scrapy
PROXY = "http://resi-lx_k3m9q2p7xa:[email protected]:7777"classWhoamiSpider(scrapy.Spider):
name = "whoami"asyncdefstart(self):
# the proxy goes in meta; Scrapy sends the login for youyield scrapy.Request("https://api.ludaxproxy.com/whoami", meta={"proxy": PROXY})
defparse(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:
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.
One file and five settings, and every request in the project goes through Ludax. Your spiders stay as they are.
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"].
The country for every request in one setting.
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.
A proxy in meta still wins, for the odd request that needs another country.
middlewares.py
import secrets
from scrapy.core.downloader.handlers.http11import TunnelError
from scrapy.exceptionsimport IgnoreRequest
from scrapy.utils.deferimport 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.useror"-NAE-"in self.user# a TLS port
self.spider_token = secrets.token_hex(4)
@classmethod
deffrom_crawler(cls, crawler):
returncls(crawler)
defprocess_request(self, request):
if"proxy"in request.meta:
returnNone# the spider chose a proxy itselfif 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 browserelse:
# 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}"returnNonedefrefused(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"))
raiseIgnoreRequest(f"Ludax refused the login (407): {detail}")
defprocess_response(self, request, response):
if response.status == 407: # http:// URLs get the gateway's answer as a response
self.refused(request.url)
return response
defprocess_exception(self, request, exception):
ifisinstance(exception, TunnelError) and"'status': 407"instr(exception):
self.refused(exception) # https:// URLs get it as a tunnel errorreturnNone
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
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.
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.
On the TLS gateways Scrapy needs no certificate setup: it does not check certificates by default. The middleware adds -sameip for sticky sessions. Why the TLS ports?
[ 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
classAccountsSpider(scrapy.Spider):
"""Each account keeps its own IP: run with LUDAX_STICKY_MINUTES = 30."""
name = "accounts"asyncdefstart(self):
for account in ["shop1", "shop2", "shop3"]:
# the same ludax_session = the same IP; letters and digits onlyfor page inrange(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,
)
defparse(self, response, account):
yield {"account": account, "ip": response.json()["ip"]}
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"classPricesSpider(scrapy.Spider):
"""The same page seen from three countries."""
name = "prices"asyncdefstart(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,
)
defparse(self, response, country):
yield {"asked": country, "got": response.json()["country"]}
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"
The TLS gateway: gw.ludaxproxy.com:9999 (Residential Premium TLS: gw.ludaxproxy.com:8081).
-AE-chrome154 at the end of the username: the browser to look like. AE also sets a matching User-Agent for you.
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.
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 see
Why
Fix
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 ... 407
Scrapy’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 allowed
The same refused login, on an http:// URL.
As above.
Could not open CONNECT tunnel ... [{'status': 502, 'reason': b'Bad Gateway'}]
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.
[ 08 / NEXT ]
Beyond Scrapy
The products behind the spiders, and guides for the jobs Scrapy runs.