Back to Blog

How to Build Your Own Proxy Rotator in Python + aiohttp

A step-by-step tutorial on building an asynchronous proxy rotator in Python using aiohttp. Handle thousands of requests with automatic proxy rotation.

Pineapple Team11 min read
pythontutorialaiohttpscrapingrotationasync

Why Build a Proxy Rotator?

When scraping at scale, websites detect and block requests from a single IP. A proxy rotator distributes requests across a pool of proxies, making your traffic appear to come from many different users.

Building your own gives you full control over rotation strategy, error handling, and proxy quality filtering.

The Architecture

Request Queue → Proxy Rotator → Random Proxy Selection → aiohttp Request ↓ Success? → Return Response ↓ Fail? → Mark Proxy → Remove from Pool

Basic Setup

import asyncio
import aiohttp
import random
from dataclasses import dataclass
from typing import Optional

@dataclass
class Proxy:
    url: str
    protocol: str  # http, https, socks5
    failures: int = 0
    max_failures: int = 3

Proxy Pool Manager

class ProxyPool:
    def __init__(self, proxies: list[Proxy]):
        self._proxies = proxies.copy()
        self._alive = proxies.copy()

    def get_random(self) -> Optional[str]:
        if not self._alive:
            self._refill()
        if not self._alive:
            return None
        proxy = random.choice(self._alive)
        return f"{proxy.protocol}://{proxy.url}"

    def mark_failed(self, proxy_url: str):
        for p in self._alive:
            if f"{p.protocol}://{p.url}" == proxy_url:
                p.failures += 1
                if p.failures >= p.max_failures:
                    self._alive.remove(p)
                break

    def mark_success(self, proxy_url: str):
        for p in self._alive:
            if f"{p.protocol}://{p.url}" == proxy_url:
                p.failures = 0
                break

    def _refill(self):
        self._alive = [
            p for p in self._proxies
            if p.failures < p.max_failures
        ]

Async Request Handler

class ProxyRotator:
    def __init__(self, pool: ProxyPool, max_retries: int = 3):
        self.pool = pool
        self.max_retries = max_retries

    async def fetch(self, session: aiohttp.ClientSession,
                    url: str, **kwargs) -> Optional[str]:
        for attempt in range(self.max_retries):
            proxy = self.pool.get_random()
            if not proxy:
                return None

            try:
                async with session.get(
                    url, proxy=proxy, timeout=aiohttp.ClientTimeout(10), **kwargs
                ) as response:
                    if response.status == 200:
                        self.pool.mark_success(proxy)
                        return await response.text()
                    else:
                        self.pool.mark_failed(proxy)
            except Exception:
                self.pool.mark_failed(proxy)

            await asyncio.sleep(0.5 * (attempt + 1))

        return None

Putting It All Together

async def main():
    # Load your proxy list (from Pineapple Proxy API, file, etc.)
    proxy_list = [
        Proxy("123.45.67.89:8080", "http"),
        Proxy("98.76.54.32:3128", "https"),
        Proxy("11.22.33.44:1080", "socks5"),
    ]

    pool = ProxyPool(proxy_list)
    rotator = ProxyRotator(pool, max_retries=3)

    urls = [
        "https://httpbin.org/ip" for _ in range(20)
    ]

    async with aiohttp.ClientSession() as session:
        tasks = [rotator.fetch(session, url) for url in urls]
        results = await asyncio.gather(*tasks)

    for i, result in enumerate(results):
        print(f"Request {i}: {result[:50] if result else 'Failed'}")

asyncio.run(main())

Advanced Features

1. Weighted Random Selection

Prefer faster proxies by weighting selection by response time:

def get_weighted(self) -> Optional[str]:
    if not self._alive:
        return None
    weights = [1.0 / (p.response_time or 1) for p in self._alive]
    proxy = random.choices(self._alive, weights=weights, k=1)[0]
    return f"{proxy.protocol}://{proxy.url}"

2. Concurrent Request Limiting

Avoid flooding a single proxy:

class ThrottledProxy(Proxy):
    concurrent_requests: int = 0
    max_concurrent: int = 2

3. Geo-Aware Routing

Route requests to proxies in specific countries:

def get_by_country(self, country: str) -> Optional[str]:
    candidates = [p for p in self._alive if p.country == country]
    if not candidates:
        return None
    proxy = random.choice(candidates)
    return f"{proxy.protocol}://{proxy.url}"

Using Pineapple Proxy as Your Source

Instead of maintaining your own proxy discovery and validation, use our Proxy List API to get a pre-verified pool:

import requests

response = requests.get(
    "https://pproxy.tech/api/ProxyDemo/Index",
    headers={"Accept": "application/json"}
)
proxies_data = response.json()

proxy_list = [
    Proxy(
        item["discovered"]["proxyPoint"]["ipPort"],
        item["discovered"]["proxyPoint"]["protocol"].lower()
    )
    for item in proxies_data
]

Conclusion

Building a proxy rotator is straightforward with Python and aiohttp. The key is robust error handling, smart retry logic, and a reliable proxy source. Whether you use our API or public lists, the architecture remains the same.

Need a ready-to-use proxy pool? Browse our verified proxies or check our plans.