Conocimiento

Cómo usar proxies residenciales con Python (requests, httpx, Scrapy)

Una guía práctica para usar proxies residenciales en Python: autenticación, IPs rotativas vs sticky, geo-targeting, reintentos y ejemplos completos para requests, httpx y Scrapy.

Chris Collins

Chris Collins

21 de junio de 2026 · 9 min de lectura

Conectar un proxy residencial a un script de Python es trabajo de cinco minutos una vez que has visto cómo se hace. La fricción nunca es el código, son los detalles que nadie anota: cómo las credenciales codifican el targeting, cuándo rotar frente a fijar una IP, cómo manejar los errores específicos del proxy, y las pequeñas diferencias de librería entre requests, httpx y Scrapy.

Esta es la guía que ojalá hubiera tenido. Ejemplos para copiar y pegar que de verdad corren, para las tres librerías que probablemente usas, más las partes que convierten un snippet que funciona en un scraper que sobrevive a producción.

Todo lo de abajo usa el gateway residencial de Shifter: un endpoint, p.shifter.io:443, con todo el targeting codificado en el nombre de usuario. Si estás en otro proveedor la forma es la misma; cambia el host y el formato de credenciales.

Lo único que tienes que entender primero

En un gateway residencial, el nombre de usuario del proxy lleva tu autenticación y tu targeting. No cambias de endpoint para cambiar de país o de sesión, cambias la cadena del nombre de usuario. Un nombre de usuario se ve así:

customer-USERNAME-country-us-sid-abc123-ttl-600

Léelo de izquierda a derecha: id de cuenta, luego flags. country-us apunta a EE. UU. sid-abc123 fija una sesión sticky. ttl-600 mantiene esa IP durante 600 segundos. Quita sid/ttl y cada petición rota a una IP nueva. La contraseña es constante. Ese es todo el modelo mental, el resto es solo enchufar esta cadena en el hueco de proxy de cada librería.

Mantén las credenciales en variables de entorno, nunca hardcodeadas:

import os
USER = os.environ["SHIFTER_USER"] # tu nombre de usuario de cuenta
PASS = os.environ["SHIFTER_PASS"]
GATEWAY = "p.shifter.io:443"

requests: la versión de 6 líneas

requests toma un dict proxies. Ambas claves http y https apuntan a la misma URL de proxy http://, eso es correcto, requests tuneliza HTTPS a través de ella vía CONNECT.

import os, requests
USER = os.environ["SHIFTER_USER"]
PASS = os.environ["SHIFTER_PASS"]
GATEWAY = "p.shifter.io:443"
def proxy(country="us"):
url = f"http://{USER}-country-{country}:{PASS}@{GATEWAY}"
return {"http": url, "https": url}
r = requests.get("https://api.ipify.org?format=json", proxies=proxy("us"), timeout=30)
print(r.json()) # {'ip': '<una IP residencial de EE. UU.>'}

Córrelo dos veces y obtendrás dos IPs distintas, porque no hay sid, cada petición rota. Ese es el valor por defecto, y para la mayoría del scraping es exactamente lo que quieres.

Rotativo vs sticky, en código

Esta es la distinción que hace tropezar a la gente, así que aquí está en concreto. (Para la versión conceptual, ve proxies residenciales sticky vs rotativos.)

Rotativo (IP nueva en cada petición) es el valor por defecto, solo omite sid:

for _ in range(3):
r = requests.get("https://api.ipify.org", proxies=proxy("us"), timeout=30)
print(r.text) # tres IPs distintas

Sticky (la misma IP a lo largo de peticiones) necesita un id de sesión y un TTL en el nombre de usuario. Úsalo cuando un flujo abarca varias peticiones que deben parecer un solo usuario, un login, luego las páginas detrás de él:

def sticky_proxy(country="us", session="s1", ttl=600):
url = f"http://{USER}-country-{country}-sid-{session}-ttl-{ttl}:{PASS}@{GATEWAY}"
return {"http": url, "https": url}
s = requests.Session()
p = sticky_proxy(session="checkout-42", ttl=600)
for path in ("/login", "/cart", "/checkout"):
r = s.get(f"https://shop.example{path}", proxies=p, timeout=30)
# las tres peticiones comparten una IP hasta 600s

Elige el id de sesión tú mismo, cualquier cadena única por sesión lógica. La misma cadena devuelve la misma IP hasta que expira el TTL, luego rota manteniendo tus otros flags.

Geo-targeting

Como el targeting vive en el nombre de usuario, la geografía es solo otro flag. El país es el común; estado, ciudad y ASN funcionan igual:

def geo_proxy(country, city=None):
parts = [USER, "country", country]
if city:
parts += ["city", city.lower().replace(" ", "_")]
url = f"http://{'-'.join(parts)}:{PASS}@{GATEWAY}"
return {"http": url, "https": url}
requests.get("https://example.com", proxies=geo_proxy("de")) # Alemania
requests.get("https://example.com", proxies=geo_proxy("us", "new york")) # Nueva York

Los nombres de ciudad usan guiones bajos para los espacios (new_york). Combina flags libremente; el orden no le importa al gateway.

httpx: la misma idea, lista para async

httpx es la opción moderna cuando quieres async o HTTP/2. El proxy va en el cliente. Fíjate en que el parámetro es proxy= (singular) en el httpx actual; las versiones antiguas usaban proxies=.

import os, httpx
USER = os.environ["SHIFTER_USER"]
PASS = os.environ["SHIFTER_PASS"]
GATEWAY = "p.shifter.io:443"
def proxy_url(country="us"):
return f"http://{USER}-country-{country}:{PASS}@{GATEWAY}"
# Síncrono
with httpx.Client(proxy=proxy_url("us"), timeout=30) as client:
print(client.get("https://api.ipify.org").text)

La versión async es donde httpx se gana el sueldo, despliega muchas peticiones concurrentes, cada una a través de una IP rotativa nueva:

import asyncio, httpx
async def fetch(client, url):
r = await client.get(url, timeout=30)
return r.status_code, r.text[:80]
async def main(urls):
async with httpx.AsyncClient(proxy=proxy_url("us")) as client:
return await asyncio.gather(*(fetch(client, u) for u in urls))
urls = ["https://api.ipify.org"] * 10
print(asyncio.run(main(urls))) # 10 peticiones concurrentes, IPs rotativas

Eso son diez peticiones residenciales concurrentes en una docena de líneas. Cuida tu concurrencia, más no siempre es más rápido, y machacar un objetivo desde una ráfaga de IPs todavía puede activar la detección por comportamiento.

Scrapy: un middleware de proxy

Scrapy es el peso pesado para crawls grandes. La forma limpia de adjuntar proxies es fijar request.meta["proxy"] por petición, el HttpProxyMiddleware integrado de Scrapy lo lee y maneja la cabecera Proxy-Authorization a partir de las credenciales en la URL.

Un pequeño middleware que rota geo y da a cada petición una IP nueva:

middlewares.py
import os
class ResidentialProxyMiddleware:
def __init__(self):
self.user = os.environ["SHIFTER_USER"]
self.password = os.environ["SHIFTER_PASS"]
self.gateway = "p.shifter.io:443"
def process_request(self, request, spider):
country = request.meta.get("country", "us")
request.meta["proxy"] = (
f"http://{self.user}-country-{country}:"
f"{self.password}@{self.gateway}"
)

Actívalo en settings.py (debe correr antes del middleware de proxy por defecto):

settings.py
DOWNLOADER_MIDDLEWARES = {
"myproject.middlewares.ResidentialProxyMiddleware": 350,
"scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware": 400,
}

Ahora cualquier spider enruta a través de IPs residenciales, y puedes apuntar por petición con Request(url, meta={"country": "gb"}). Para una sesión sticky, construye el nombre de usuario con un sid/ttl exactamente como en el ejemplo de requests y fíjalo en meta["proxy"].

Manejar los errores que de verdad ocurren

Un scraper que ignora los errores de proxy funciona en la demo y muere de madrugada. Tres que encontrarás:

  • 407 Proxy Authentication Required, usuario/contraseña incorrectos, o un flag no reconocido (una errata en country o asn). Arregla la cadena de credenciales; reintentar no ayudará.
  • 429 Too Many Requests, el objetivo te está limitando la tasa. Haz backoff, ve más despacio, y rota IPs (cosa que el gateway ya hace por petición cuando no eres sticky).
  • 502 Bad Gateway, ninguna IP coincide ahora mismo con tu filtro (normalmente geo demasiado ajustado como country+city+asn). Afloja un flag y reintenta.

Un wrapper de reintento mínimo que hace backoff y se rinde limpiamente:

import time, requests
def get_with_retry(url, proxies, tries=4):
for attempt in range(tries):
try:
r = requests.get(url, proxies=proxies, timeout=30)
if r.status_code in (429, 502, 503):
raise requests.exceptions.RequestException(f"status {r.status_code}")
r.raise_for_status()
return r
except requests.exceptions.RequestException as e:
if attempt == tries - 1:
raise
sleep = 2 ** attempt # 1s, 2s, 4s
print(f"retry {attempt+1}: {e}; sleeping {sleep}s")
time.sleep(sleep)

Backoff exponencial más rotación por petición maneja la gran mayoría de los fallos transitorios. Si te están bloqueando consistentemente en lugar de intermitentemente, el problema no son los reintentos, es la calidad de la IP o el comportamiento de las peticiones, cubierto en cómo evitar que te bloqueen al hacer scraping.

Verificar que de verdad funciona

Antes de confiar en una configuración, confirma dos cosas: que la IP cambia (rotación) y que está en el país correcto (geo). Una comprobación rápida:

import requests
r = requests.get("http://ip-api.com/json", proxies=proxy("de"), timeout=30)
data = r.json()
print(data["query"], data["countryCode"]) # se espera una IP DE

Llámala unas cuantas veces sin un sid y deberías ver IPs distintas, todas DE. Si el país es incorrecto, revisa la ortografía de tu flag. Si la IP nunca cambia, dejaste un sid por accidente en el nombre de usuario.

Una nota sobre SOCKS5

Todo lo de arriba usa proxies HTTP, el valor por defecto correcto para scraping web. Si tu carga necesita SOCKS5 (tráfico no HTTP, o una herramienta que lo espera), el mismo gateway lo habla, cambia el esquema a socks5h:// e instala requests[socks] (o usa el extra de SOCKS de httpx). Los tradeoffs están en HTTP vs SOCKS5 proxies; para scraping simple, quédate con HTTP.

Preguntas frecuentes

¿Necesito un endpoint distinto para cada país? No. Un endpoint, p.shifter.io:443, para todo. País, ciudad y sesión cambian todos vía la cadena del nombre de usuario, no el host. Ese es el núcleo del modelo de gateway.

¿Por qué ambas claves http y https apuntan a una URL http:// en requests? Porque requests envía HTTPS a través de un proxy HTTP vía un túnel CONNECT. El esquema de la URL del proxy describe cómo hablas con el proxy (HTTP), no con el objetivo. Esto es correcto y estándar, no pongas la clave https en https://.

¿Cómo roto IPs en cada petición? Omite sid del nombre de usuario. Sin id de sesión, el gateway te da una IP nueva por petición automáticamente. No gestionas una lista de proxies, la rotación del pool es del lado del servidor.

¿Cómo mantengo la misma IP a lo largo de un flujo multi-paso? Añade sid-<tu-id>-ttl-<segundos> al nombre de usuario y reúsalo en cada petición del flujo. El mismo id, la misma IP, hasta que expire el TTL.

Mis peticiones son lentas. ¿Es el proxy el problema? Normalmente no. Las IPs residenciales añaden algo de latencia frente a una conexión directa, pero la lentitud a escala más a menudo es demasiada concurrencia, no reusar conexiones (usa un Session/Client), o un objetivo lento. Perfila antes de culpar al proxy.

¿Funciona también con aiohttp, urllib3 o pycurl? Sí. Cualquier cliente que acepte una URL de proxy autenticada http://user:pass@host:port funciona, el targeting codificado en las credenciales es idéntico. requests, httpx y Scrapy son solo los tres más comunes.

Cerrando

El patrón es el mismo en cada librería: construye la URL http://USER-flags:PASS@p.shifter.io:443, ponla en el hueco de proxy, omite sid para rotar o añádelo para fijar. La geo es un flag, los errores son un pequeño bucle de reintento, y la concurrencia es lo que tu cliente ya hace.

Empieza desde los snippets de arriba, apúntalos al gateway residencial, y tienes código de proxy con forma de producción en una tarde. Los planes y las tarifas por GB están en la página de precios, y la referencia completa de flags vive en la documentación del gateway.

Etiquetas: python residential proxies requests httpx scrapy web scraping

¿Listo para empezar?

Prueba los proxies residenciales de Shifter, más de 205M IPs, más de 195 países, desde 1,00 $/GB.

Comenzar