DEV Community

App CyberYozh
App CyberYozh

Posted on

Stop Using time.sleep(): Architecting Resilient Python Scrapers with HTTP Adapters

When building automated data extraction pipelines, the network is your biggest enemy. Target servers drop connections, proxy rotations introduce latency, and WAFs throw 429 Too Many Requests errors.

The immediate instinct for most developers is to wrap their requests.get() calls in a massive try/except block with a time.sleep(5) thrown in. This is a massive anti-pattern that leads to stalled queues, ghost processes, and bloated codebases.

Let’s refactor a fragile scraping script into a production-ready engine using native urllib3 retry logic.


❌ The Anti-Pattern: Manual Retry Loops

Here is what typical beginner scraping code looks like. It is messy, blocks the main thread unnecessarily, and doesn't handle specific HTTP status codes elegantly:

import requests
import time

def fetch_data_bad(url):
    for attempt in range(3):
        try:
            response = requests.get(url)
            if response.status_code == 200:
                return response.json()
            elif response.status_code == 429:
                print("Rate limited! Sleeping...")
                time.sleep(10)
        except requests.exceptions.ConnectionError:
            time.sleep(5)
    return None
Enter fullscreen mode Exit fullscreen mode

βœ…The Best Practice: Mounting an HTTPAdapter

The Python requests library is built on top of urllib3, which has a built-in, highly robust retry utility. Instead of writing manual loops, we can configure a Session object to automatically handle exponential backoff and status-code-specific retries at the transport layer.

Here is the clean, enterprise-grade approach:

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

def create_resilient_session():
    session = requests.Session()

    # Define the retry strategy
    retry_strategy = Retry(
        total=5,  # Maximum number of retries
        backoff_factor=2,  # Exponential backoff multiplier (1s, 2s, 4s, 8s...)
        status_forcelist=[413, 429, 500, 502, 503, 504], # Status codes to retry on
        allowed_methods=["HEAD", "GET", "OPTIONS"] 
    )

    # Mount the adapter to both HTTP and HTTPS
    adapter = HTTPAdapter(max_retries=retry_strategy)
    session.mount("https://", adapter)
    session.mount("http://", adapter)

    return session

# Usage:
scraper = create_resilient_session()
# If this fails or hits a 429, it automatically waits and retries under the hood!
response = scraper.get("[https://api.target-site.com/data](https://api.target-site.com/data)")
Enter fullscreen mode Exit fullscreen mode

Why This Architecture Matters

  1. Exponential Backoff (backoff_factor): If a target server is overwhelmed, slamming it with immediate retries will get your IP banned. A backoff factor of 2 means your script will wait 1, 2, 4, 8, and 16 seconds between attempts, mimicking organic human delays.
  2. Global Session Application: You mount the adapter once. Every single .get() or .post() request you make using that session automatically inherits the retry logic.
  3. Proxy Optimization: When routing traffic through rotating residential proxy networks, connections occasionally drop mid-rotation. An HTTPAdapter automatically re-establishes the connection to the next proxy node without your scraper ever throwing an exception.

πŸ’‘ Resource: For a complete guide on integrating advanced proxy authentication with this retry logic, check out the full tutorial: Optimizing Python Requests with Retry Logic.

By pushing error handling down to the transport layer, your business logic stays clean, and your pipelines become infinitely more stable.

Have you integrated urllib3.util.retry into your pipelines, or are you using external libraries like Tenacity? Share your stack in the comments!

Top comments (0)