How to Rotate a List of Static Proxies in Python
Updated:
To rotate proxies in Python you need three things: a file of proxies in ip:port:user:pass format, a function that turns each line into an http://user:pass@ip:port URL, and a selector that picks a proxy for each request, whether round-robin, random or pinned to a session. You do not need a rotating proxy service for this. With a pool of dedicated static IPs, your code decides which IP sends which request, how often to switch, and when to bench an IP that misbehaves. This guide covers loading the list, three rotation strategies, error handling and concurrency, with sample code for the requests library.
Rotating static IPs vs a rotating proxy service
A rotating proxy service gives you one gateway and changes the exit IP behind it per request or on a timer. You do not know which IP comes next and you cannot keep one you like. That suits very high-volume crawling, but it makes it hard to know exactly which IP sent which request. For the full comparison, read static vs rotating proxies.
When you rotate your own list of static IPs, every decision lives in your code. Each IP is yours alone, so its reputation depends on how you use it. You can log the IP behind every request, bench an IP when a site starts returning errors, and keep one IP for a logged-in session. The trade-off is that you write the selection, retry and throttling logic yourself. The rest of this guide is the minimal code to do that properly.
Step 1: prepare the proxy list
Proxies are delivered as ip:port:user:pass, one per line. Save them to proxies.txt. The requests library expects URLs, so convert once when you load the file. If a password contains characters such as @, : or #, URL-encode it; otherwise requests parses the URL wrongly and you get a 407 Proxy Authentication Required error. You can also convert a whole list with the proxy format converter.
from urllib.parse import quotedef to_url(line):ip, port, user, pw = line.strip().split(':', 3)return 'http://' + quote(user, safe='') + ':' + quote(pw, safe='') + '@' + ip + ':' + portPROXIES = [to_url(l) for l in open('proxies.txt') if l.strip()]- Use the
http://scheme for both thehttpandhttpskeys of theproxiesdict. For HTTPS sites, requests opens a CONNECT tunnel through the HTTP proxy, so the proxy itself does not need to speak TLS.
Step 2: three ways to rotate
No single strategy fits every job. Public pages without a login can be spread evenly across IPs, while anything with a session, cart or login should keep one IP for the whole session.
- Round-robin example:
for url in urls: p = next(pool); r = requests.get(url, proxies={'http': p, 'https': p}, timeout=(5, 20)) - Sticky example: set
keyto an account name or domain and the same key always maps to the same IP, even after a restart. Usezlib.crc32rather thanhash(), because Python randomiseshash()between runs.
| Strategy | Core code | Use it when |
|---|---|---|
| Round-robin | pool = itertools.cycle(PROXIES) then next(pool) | Crawling public pages and spreading load evenly |
| Random | random.choice(PROXIES) | Request order does not matter and you want to avoid a fixed rhythm |
| Sticky by key | PROXIES[zlib.crc32(key.encode()) % len(PROXIES)] | One account, session or domain should always use the same IP |
Step 3: one Session per proxy
Calling requests.get() for every URL opens a fresh connection and TLS handshake each time. For a group of requests on the same IP, create one requests.Session() per proxy and reuse it. A session keeps connections alive and stores cookies, which pairs naturally with sticky rotation: each session is a fixed IP plus its own cookies.
A simple pattern is a sessions = {} dict, then s = sessions.setdefault(p, requests.Session()) and s.proxies = {'http': p, 'https': p}. Avoid sharing one session while swapping proxies between requests, because cookies earned on one IP get sent from another, and the site sees a session jumping between IPs. More configuration detail is on the Python requests proxy setup page.
Step 4: retry on another IP and bench failing ones
In a list of a few dozen IPs, one will occasionally time out or receive 403 or 429 from a particular site. Good code does not stop the whole run; it moves the request to another IP and rests the one that failed. The simplest way is a cooldown = {} dict holding the time each IP may be used again.
- Catch connection errors with
except (requests.exceptions.ProxyError, requests.exceptions.ConnectTimeout, requests.exceptions.ReadTimeout), record the failure and try the next IP. - On 429 or 403, set
cooldown[p] = time.time() + 300to rest the IP for five minutes, and skip any proxy wherecooldown.get(p, 0) > time.time(). - Cap attempts, for example three IPs per URL, so one broken page cannot burn through the whole list.
- Log URL, IP, status code and response time for every request. When something goes wrong, the log tells you whether the problem is one IP, the whole pool or the target site.
- For health checks, load balancing and logging at a larger scale, see proxy pool management.
Step 5: run concurrently without overloading one IP
With concurrent.futures.ThreadPoolExecutor, scale the number of threads to the number of IPs rather than cranking it up. A safe starting rule is no more threads than IPs, with each IP handling one request at a time. A queue.Queue of idle proxies does this neatly: a thread takes a proxy, sends the request, waits briefly and puts the proxy back.
A small random delay between requests on the same IP, such as time.sleep(random.uniform(1, 3)), makes the pattern less mechanical and is kinder to the target server. The right pace depends on the site; web scraping rate limits explains how to estimate requests per IP and how many IPs a workload needs.
Checklist before a real run
- Check the whole list first with the proxy checker or a test call to an IP echo endpoint such as
requests.get('https://httpbin.org/ip', proxies=..., timeout=10). - Always set a
timeout; without one, a hung IP can block a thread forever. - Send a sensible
User-Agentand keep it consistent within a session. - Respect
robots.txtand the site's terms, and collect only public data you are allowed to use. - Keep proxy credentials out of code you push to Git; read them from a file or environment variables.
- When the rental period ends, renew from the dashboard to keep the same IPs, so your list and per-IP logs stay valid.
Frequently asked questions
Can rotating a static list replace a residential rotating proxy?
For public pages at a moderate pace it is usually enough, because the load is spread across several dedicated IPs. If a site blocks all datacenter ranges or you need thousands of distinct IPs, a small self-rotated list will not substitute. VinaProxy does not currently sell rotating residential proxies.
Should I switch IP on every request or on a timer?
Public pages without a login can switch on every request. Anything with cookies, a cart or a login should keep one IP for the whole session, since an IP change mid-session looks unusual.
Why does the proxy work in my browser but return 407 in Python?
Usually the password contains special characters that were not URL-encoded, or the user and password were split in the wrong order. Use quote(pw, safe='') as in the sample code.
Does this work with httpx or aiohttp?
The loading and selection logic stays the same. Only the way you pass the proxy differs: httpx takes it when you create the client, aiohttp takes a proxy argument per request.
Do the proxies support SOCKS5 for requests?
VinaProxy proxies use HTTP/HTTPS with username and password authentication. Use the http:// scheme shown above; no SOCKS package is needed.
Build your own pool of dedicated IPs
Private IPv4 proxies in 7 countries. Each IP goes to one customer, stays fixed for the rental, has unlimited bandwidth and ships as ip:port:user:pass.
See pricing