Back to all posts
Integrating Proxies into Apache Airflow Workflows for Scheduled Data Extraction

Integrating Proxies into Apache Airflow Workflows for Scheduled Data Extraction

July 12, 2026

Why Use Proxies in Airflow Workflows

Apache Airflow excels at orchestrating repetitive tasks such as nightly data pulls, weekly market scans, or continuous SEO monitoring. When those tasks involve hitting external websites or APIs, you quickly run into rate limits, IP bans, or geo‑restrictions. Adding a proxy layer lets you:

  • Distribute requests across many IP addresses to stay under per‑IP limits.
  • Appear as traffic from different geographic regions for localized data.
  • Hide the origin of your Airflow workers, reducing the chance of being flagged as a bot.
  • Rotate IPs automatically when a site returns a 429 or CAPTCHA response.

A quality proxy provider like RoProxy supplies residential, datacenter, or mobile IPs with built‑in rotation and authentication, making it straightforward to plug into Airflow without managing your own proxy infrastructure.

Architecture Overview

At a high level, an Airflow DAG that uses proxies follows this flow:

  1. Task Instance – A PythonOperator (or custom operator) prepares a request.
  2. Proxy Configuration – The operator reads proxy credentials from Airflow Connections or Variables.
  3. Request Execution – The request is sent through the chosen proxy using libraries such as requests, httpx, or aiohttp.
  4. Response Handling – Based on status codes or content, the operator decides whether to retry with a different IP.
  5. Logging & Monitoring – Proxy IP, latency, and outcome are logged for debugging and metrics.

This design keeps the proxy logic encapsulated, making DAGs easy to read and maintain.

Setting Up Proxy Credentials in Airflow

Airflow recommends storing secrets in Connections (encrypted in the metadata database) or using the Secrets Backend (AWS Secrets Manager, HashiCorp Vault, etc.). For this guide we’ll use a simple HTTP proxy Connection.

  1. Create a Connection

    • Go to Admin → Connections → Create.
    • Conn Id: my_proxy
    • Conn Type: HTTP
    • Host: proxy.roproxy.com
    • Port: 8000
    • Login: your_username
    • Password: your_password
    • Extra (JSON): {"proxy_type": "residential"}
  2. Reference the Connection in Code

    from airflow.hooks.base import BaseHook
    
    def get_proxy_dict():
        conn = BaseHook.get_connection('my_proxy')
        proxy_url = f"http://{conn.login}:{conn.password}@{conn.host}:{conn.port}" 
        return {"http": proxy_url, "https": proxy_url}
    

Building a Custom Proxy Operator

While you could call get_proxy_dict() inside each task, a reusable operator keeps the DAG clean. Below is a ProxyHttpOperator that extends BaseOperator and handles rotation automatically.

# proxy_http_operator.py
from airflow.models import BaseOperator
from airflow.hooks.base import BaseHook
import requests
import time

class ProxyHttpOperator(BaseOperator):
    """
    Executes an HTTP request through a configurable proxy.
    
    :param endpoint: URL to call.
    :param method: HTTP method (default GET).
    :param params: Query string parameters.
    :param headers: Request headers.
    :param proxy_conn_id: Airflow Connection ID for the proxy.
    :param max_retries: How many times to retry with a new IP on failure.
    :param retry_delay: Seconds to wait between retries.
    """
    template_fields = ('endpoint', 'params', 'headers')
    
    def __init__(self, *, endpoint, method='GET', params=None, headers=None,
                 proxy_conn_id='my_proxy', max_retries=3, retry_delay=2, **kwargs):
        super().__init__(**kwargs)
        self.endpoint = endpoint
        self.method = method.upper()
        self.params = params or {}
        self.headers = headers or {}
        self.proxy_conn_id = proxy_conn_id
        self.max_retries = max_retries
        self.retry_delay = retry_delay
    
    def _get_proxy(self):
        conn = BaseHook.get_connection(self.proxy_conn_id)
        proxy_url = f"http://{conn.login}:{conn.password}@{conn.host}:{conn.port}" 
        return {"http": proxy_url, "https": proxy_url}
    
    def execute(self, context):
        attempt = 0
        while attempt <= self.max_retries:
            proxies = self._get_proxy()
            try:
                self.log.info(f"Attempt {attempt+1} – calling {self.endpoint} via {proxies['http']}" )
                resp = requests.request(
                    method=self.method,
                    url=self.endpoint,
                    params=self.params,
                    headers=self.headers,
                    proxies=proxies,
                    timeout=30
                )
                # Treat 2xx as success, 429/403/503 as proxy‑related failures
                if resp.status_code < 300:
                    self.log.info(f"Success – status {resp.status_code}" )
                    return resp.text
                if resp.status_code in (429, 403, 503):
                    raise requests.HTTPError(f"Status {resp.status_code}" )
                # For other errors (e.g., 404) we do not rotate IP
                resp.raise_for_status()
            except Exception as e:
                attempt += 1
                self.log.warning(f"Request failed: {e}" )
                if attempt > self.max_retries:
                    self.log.error("Max retries exceeded. Raising." )
                    raise
                self.log.info(f"Waiting {self.retry_delay}s before retry…" )
                time.sleep(self.retry_delay)
        # Should never reach here
        raise RuntimeError("Unexpected exit from retry loop" )

The operator does three important things:

  • Retrieves proxy credentials from the Connection each attempt, so if your provider rotates credentials (e.g., time‑based token) you get the latest.
  • Treats HTTP 429 (Too Many Requests), 403 (Forbidden – often a block), and 503 (Service Unavailable) as signals to switch IP and retry.
  • Logs each attempt, making it easy to see which IP succeeded.

Using the Operator in a DAG

Here’s a sample DAG that scrapes a product listing page every six hours, rotating proxies automatically when needed.

# dag_proxy_scrape.py
from datetime import datetime, timedelta
from airflow import DAG
from proxy_http_operator import ProxyHttpOperator

default_args = {
    "owner": "data-eng",
    "depends_on_past": False,
    "email_on_failure": False,
    "retries": 0,  # retries handled by the operator
    "retry_delay": timedelta(minutes=5),
}

with DAG(
    dag_id="proxy_product_scrape",
    default_args=default_args,
    description="Scrape product prices using rotating proxies",
    schedule_interval="0 */6 * * *",  # every 6 hours
    start_date=datetime(2024, 1, 1),
    catchup=False,
    tags=["scraping", "proxy"],
) as dag:
    scrape_task = ProxyHttpOperator(
        task_id="scrape_product_page",
        endpoint="https://example-shop.com/products?category=electronics",
        method="GET",
        headers={
            "User-Agent": "Mozilla/5.0 (compatible; AirflowScraper/1.0)" ,
            "Accept": "text/html,application/xhtml+xml" ,
        },
        proxy_conn_id="my_proxy",
        max_retries=5,
        retry_delay=3,
    )
    scrape_task

When the DAG runs, Airflow logs will show something like:

[2024-09-25 10:00:00,123] {base_task_runner.py:115} INFO - Attempt 1 – calling https://example-shop.com/products?category=electronics via http://user:[email protected]:8000
[2024-09-25 10:00:02,456] {proxy_http_operator.py:45} WARNING - Request failed: 429 Client Error: Too Many Requests for url: https://example-shop.com/products?category=electronics
[2024-09-25 10:00:05,470] {proxy_http_operator.py:45} INFO - Attempt 2 – calling https://example-shop.com/products?category=electronics via http://user:[email protected]:8000
[2024-09-25 10:00:07,890] {proxy_http_operator.py:45} INFO - Success – status 200

Handling Authentication and Session Persistence

Some sites require login or maintain session cookies across multiple requests. In those cases you’ll want to preserve cookies between attempts while still rotating the underlying IP. You can achieve this by using a requests.Session object inside the operator.

import requests

class ProxyHttpOperator(BaseOperator):
    # … (same init as before) …
    def execute(self, context):
        session = requests.Session()
        attempt = 0
        while attempt <= self.max_retries:
            proxies = self._get_proxy()
            session.proxies.update(proxies)
            try:
                resp = session.get(self.endpoint, headers=self.headers, timeout=30)
                if resp.status_code < 300:
                    return resp.text
                if resp.status_code in (429, 403, 503):
                    raise requests.HTTPError(f"Status {resp.status_code}" )
                resp.raise_for_status()
            except Exception as e:
                attempt += 1
                self.log.warning(f"Request failed: {e}" )
                if attempt > self.max_retries:
                    raise
                self.log.info(f"Waiting {self.retry_delay}s before retry…" )
                time.sleep(self.retry_delay)

The session retains cookies, while session.proxies is refreshed each loop, giving you a new IP but the same logged‑in state.

Monitoring Proxy Performance

To ensure your proxy layer isn’t becoming a bottleneck, collect basic metrics:

  • Latency: Record resp.elapsed.total_seconds() for each attempt.
  • Success Rate: Ratio of successful attempts to total attempts.
  • IP Usage: Log the proxy host (or the IP returned by a service like https://api.ipify.org) to see distribution.

You can push these metrics to Airflow’s StatsD integration or to a monitoring system like Prometheus via a simple PostExecute hook.

from airflow.utils.stats import Stats

# Inside execute, after a successful request:
Stats.timing('proxy.latency', resp.elapsed.total_seconds())
Stats.incr('proxy.success')
# After a failed attempt:
Stats.incr('proxy.failure')

Graphing latency over time helps you spot slow proxy nodes; a rising failure rate may indicate you need to upgrade your proxy plan or adjust rotation frequency.

Best Practices

  1. Never hard‑code credentials – always use Airflow Connections or a secrets backend.
  2. Respect target site’s terms – proxy rotation does not give you carte blanche to scrape aggressively; keep request rates reasonable and add User‑Agent strings that identify your bot.
  3. Use sticky sessions when needed – if a multi‑step flow (login → search → download) requires the same IP for the duration, set max_retries=0 for those steps and handle rotation only between distinct flows.
  4. Test with a small subset – run the DAG against a test endpoint first to verify proxy auth and error handling.
  5. Handle CAPTCHAs gracefully – if you encounter a CAPTCHA page, treat it as a failure, rotate IP, and consider adding a manual solve step or switching to a provider that offers CAPTCHA‑resistant residential IPs.
  6. Limit concurrent workers – too many parallel tasks using the same proxy pool can exhaust the pool; adjust pool settings in your Airflow config or use separate proxy Connections per worker group.

Conclusion

Integrating proxies into Apache Airflow transforms fragile, IP‑bound scraping jobs into resilient, scalable data pipelines. By encapsulating proxy logic in a custom operator, you gain:

  • Automatic IP rotation on throttling or blocking.
  • Centralized credential management via Airflow Connections.
  • Transparent session handling for sites that require authentication.
  • Built‑in logging and metrics for performance tuning.

With a reliable provider like RoProxy supplying a diverse pool of residential, datacenter, or mobile IPs, you can focus on the business logic of your DAGs while the proxy layer handles the noisy reality of the web. Start small, monitor the metrics, and iteratively tune retry delays, pool sizes, and request rates to achieve the optimal balance between speed and stealth.

Happy airflow‑powered scraping!