Skip to main content

Command Palette

Search for a command to run...

Building a Resilient Proxy Pool Manager in Python

A practical pattern for tracking proxy health, rotating on failure, and keeping bad IPs out of your request cycle

Updated
3 min readView as Markdown
Building a Resilient Proxy Pool Manager in Python
E
Ethan Carter writes about proxy infrastructure, scraping performance, and scalable data collection. He works on technical content focused on datacenter proxies, session management, bandwidth efficiency, and the operational decisions behind reliable high-volume workflows.

A proxy pool manager's job is simple: hand out a working proxy, notice when one stops working, and stop handing that one out for a while.

Most homemade pools skip the second and third parts. They just round-robin through a list and burn requests on dead IPs until someone notices the failure rate. Here's a pattern that fixes that, in plain Python with no extra dependencies beyond requests.

The core idea

Every proxy gets a small health record: how many times it's failed in a row, and when it's allowed back into rotation after being marked bad.

import time
import random
import requests

class Proxy:
    def __init__(self, address):
        self.address = address
        self.fail_count = 0
        self.cooldown_until = 0

    def is_available(self):
        return time.time() >= self.cooldown_until

    def mark_success(self):
        self.fail_count = 0

    def mark_failure(self):
        self.fail_count += 1
        # Exponential backoff: 30s, 60s, 120s, 240s...
        cooldown_seconds = 30 * (2 ** (self.fail_count - 1))
        self.cooldown_until = time.time() + min(cooldown_seconds, 1800)

Each failure pushes the cooldown further out, capped at 30 minutes. A proxy that fails once gets a short timeout. A proxy that keeps failing gets pushed further and further to the back of the line without being permanently removed, it might recover.

The pool manager

class ProxyPool:
    def __init__(self, addresses):
        self.proxies = [Proxy(addr) for addr in addresses]

def get_proxy(self):
    available = [p for p in self.proxies if p.is_available()]
    if not available:
        raise RuntimeError("No proxies available — all in cooldown")
    return random.choice(available)

def request(self, url, **kwargs):
    proxy = self.get_proxy()
    try:
        response = requests.get(
            url,
            proxies={"http": proxy.address, "https": proxy.address},
            timeout=10,
            **kwargs
        )
        if response.status_code >= 500 or response.status_code == 429:
            proxy.mark_failure()
        else:
            proxy.mark_success()
        return response
    except requests.exceptions.RequestException:
        proxy.mark_failure()
        raise

get_proxy() only pulls from proxies that are currently out of cooldown. request() wraps the actual call, marking success or failure based on what comes back, including a distinct check for 429, since a rate-limit response means the proxy is fine but overused, not broken.

Why random selection instead of round-robin

Round-robin looks predictable to a target site: proxy A, then B, then C, then A again, on a fixed clock. Random selection from the available pool breaks that pattern without adding complexity.

If you need weighted selection later, favoring proxies with lower fail counts, for example, that's a small change to get_proxy(), not a rewrite.

Handling total pool exhaustion

If every proxy is in cooldown at once, get_proxy() raises instead of silently hanging. That's deliberate. A silent retry loop against an empty pool just burns CPU and time. Better to fail loud, log it, and let the caller decide whether to wait, alert, or fall back to a different pool.

try:
    response = pool.request("https://example.com/data")
except RuntimeError:
    print("Pool exhausted — pausing before retry")
    time.sleep(60)

Where this goes from here

This version tracks failure and cooldown per proxy, which covers most small-to-medium scraping jobs. At higher volume, the next additions are usually:

  • Persisting proxy health to Redis so multiple workers share one pool state

  • Splitting the pool by target domain, since a proxy can be healthy for one site and banned on another

  • Swapping the fixed backoff formula for one tuned to your actual failure patterns

The pattern doesn't change. You're still just tracking who's healthy, who isn't, and how long the unhealthy ones sit out. Providers that expose large datacenter proxy blocks, like InstantProxies, give this kind of pool manager enough addresses to make the cooldown math actually work: a pool of five proxies doesn't have room to rotate anyone out.

O

The 429 thing bugs me most, your text says it just means overuse but the code treats it exactly like a 5xx and gives it the same escalating cooldown as a dead proxy. Retry after headers are also messier than people expect, sometimes missing, sometimes a full date instead of seconds, and 503s throw them too plus the limit might be on your account not the proxy so a separate throttle counter would help. Also that 403 branch quietly calls mark_success and wipes the fail streak even though 403 could mean banned, bad auth or just a url everyone gets blocked on. While you're in there, lok fail_count if this runs across threads, and narrow that bare except requestexception down to connectionerror and Timeout or you'll end up blaming the proxy for stuff like invalidURL that's actually your own mistake