Skip to content
LightningBytes
Back to Blog

What Is a Proxy Pool and How to Build One

A proxy pool is the set of endpoints a scraper draws from. Here is how to design one with health checks, cooldowns and per-target concurrency control.

by LightningBytes Team
  • proxy-management
  • proxy-rotation
  • web-scraping

A proxy pool is the collection of proxy endpoints your application draws from when it needs to send a request. It might be ten sticky sessions, a thousand rotating addresses, or a mix of both. The pool is the layer where you decide which identity a given request uses, when to retry, and when to give up on an endpoint.

Most scraping failures that get blamed on proxies are actually pool design problems.

What a pool does

At a minimum, a pool tracks which endpoints are available and hands them out. A good one also does four more things:

  • Tracks health, so failing endpoints stop being handed out.
  • Applies cooldowns, so a temporarily blocked endpoint gets a rest instead of being hammered.
  • Enforces concurrency, so you do not push ten parallel requests through one IP.
  • Attributes results, so you can tell an endpoint failure from a target failure.

Designing the allocation

Two decisions shape everything else.

How much continuity does the work need? If requests are independent, hand out endpoints per request. If a unit of work has state, such as a login, bind an endpoint to that unit for its duration. This is the sticky versus rotating question, covered in Rotating vs Sticky Proxies.

How much concurrency per endpoint? One. Sending several simultaneous requests from one address defeats the purpose of having many addresses, and it is a behaviour no real user exhibits. If you need throughput, add endpoints rather than parallelism per endpoint.

A simple pool in practice

The logic can be small. A sketch in Python:

import random
import time
from dataclasses import dataclass, field

@dataclass
class Endpoint:
    url: str
    failures: int = 0
    cooldown_until: float = 0.0
    in_use: int = 0

    def available(self) -> bool:
        return time.monotonic() >= self.cooldown_until

class Pool:
    def __init__(self, endpoints: list[str], max_failures: int = 3):
        self.endpoints = [Endpoint(url=e) for e in endpoints]
        self.max_failures = max_failures

    def acquire(self) -> Endpoint | None:
        candidates = [e for e in self.endpoints if e.available() and e.in_use == 0]
        if not candidates:
            return None
        endpoint = random.choice(candidates)
        endpoint.in_use += 1
        return endpoint

    def release(self, endpoint: Endpoint, ok: bool) -> None:
        endpoint.in_use -= 1
        if ok:
            endpoint.failures = 0
            return
        endpoint.failures += 1
        if endpoint.failures >= self.max_failures:
            # Back off this endpoint instead of removing it forever.
            endpoint.cooldown_until = time.monotonic() + 60
            endpoint.failures = 0

That is a few dozen lines and it captures the essentials: random selection, one request at a time per endpoint, failure counting, and a cooldown rather than permanent removal.

What to add for production

Differentiated failure handling. A connection error is an endpoint problem. A 403 or a challenge is the target reacting to the request or the IP. They deserve different responses, and treating them the same rotates you away from working addresses for no reason. See Rate Limiting vs Blocking.

Per-target isolation. Do not let one target's failures quarantine endpoints for a different target. Track health per endpoint and target pair, because an address can be fine on one site and blocked on another.

Metrics. Success rate and latency per endpoint, exported somewhere you can see trends. The signals worth watching are covered in Monitoring Scraper Health.

Session lifecycle. For sticky pools, an explicit way to release a session and mint a new IP when a unit of work completes or fails.

Global concurrency limits. Some targets throttle based on total load rather than per IP, so cap overall request rate as well as per-endpoint.

Common mistakes

  • Unlimited retries. A retry storm turns a soft rate limit into a hard block.
  • No cooldown. Retrying a dead endpoint immediately wastes the most time per unit of work.
  • Blaming the pool for parser bugs. If selectors are wrong, every endpoint looks broken. Separate data errors from transport errors in your metrics.
  • Reusing an endpoint across identities. If two accounts share an address, they are linked at the network level.
  • Ignoring the target's Retry-After. When a server tells you how long to wait, waiting is the fastest path back to success.

Build or use a managed pool

If you are using a provider's rotating gateway, much of this is handled for you: the gateway selects the exit IP and applies session semantics from your credentials. You still own the parts that matter to your application, namely per-target concurrency, retry policy and metrics.

LightningBytes exposes a single gateway with session-based stickiness, so a pool can be expressed as a set of credentials rather than a list of IPs. That model is described on the residential and mobile pages, and Understanding Proxy Session IDs explains how session values map to addresses.

Start working with cleaner IPs

Clean, pre-filtered residential and mobile proxies, sign up and send your first request in minutes.

We use cookies for authentication and security. With your consent we also enable optional marketing & analytics cookies. See our privacy policy.