---
title: "Max Retries Exceeded With URL: qué es y cómo solucionarlo"
description: "«Max Retries Exceeded With URL» envuelve en Requests el error de conexión real. La causa está en la parte Caused by; aquí ves cómo leerla y corregirla."
url: https://proxynet.io/es/blog/max-retries-exceeded-with-url
date: 2026-09-24
author: "Acar Diveroli"
category: "Tutoriales, Web scraping"
lang: es
---

# Max Retries Exceeded With URL: qué es y cómo solucionarlo

Tu script de seguimiento de precios funcionó toda la noche sin problemas. Esta mañana pegaste en él la dirección del proxy de tu nuevo plan, y ahora la terminal imprime una sola línea larga: `HTTPSConnectionPool(host='example.com', port=443): Max retries exceeded with url`. El script no tiene ninguna configuración de reintentos y el host del mensaje es la tienda de la que extraes los datos, así que la mirada va directa a la tienda. Al final de la línea, entre paréntesis, pone `Tunnel connection failed: 403 Forbidden`, y esa respuesta vino del proxy, no de la tienda.

Esta guía desmonta el mensaje: por qué dice «max retries» si no configuraste ninguno, una tabla de lectura hecha con cadenas de error que generamos nosotros mismos, las variantes con proxy, los tiempos de espera y el mismo error en pip. Termina con un script de Python probado que nombra la causa.

> **Nota: Respuesta breve**
>
> «Max retries exceeded with url» es el texto de `MaxRetryError` de urllib3 y por sí solo no nombra ninguna causa. Requests no reintenta por defecto, así que también lo ves tras un único intento fallido. La causa está en la parte `Caused by`, entre paréntesis: `NameResolutionError` significa que un nombre no se resolvió, `NewConnectionError` que la conexión fue rechazada, `ConnectTimeoutError` que no hubo conexión dentro del tiempo de espera, `SSLError` que no se pudo verificar el certificado y `ProxyError` que el problema está entre tú y el proxy. `Tunnel connection failed: 403` significa que el proxy rechazó tu petición CONNECT. Comprueba que la URL del proxy empiece por `http://`, revisa el puerto y pasa `timeout=(connect, read)` en cada petición.

## ¿Qué significa «Max retries exceeded with url»?

El mensaje lo genera urllib3, la biblioteca que abre las conexiones de Requests. Cuando urllib3 da por perdida una petición, lanza `MaxRetryError`. Requests la captura y lanza su propia excepción, elegida según la causa interna: `ConnectTimeout`, `ProxyError`, `SSLError`, `RetryError` cuando se agotan los reintentos por código de estado, o un simple `ConnectionError` para todo lo demás. Esta es una cadena de nuestra prueba, dividida en sus cuatro partes:

```text
requests.exceptions.ProxyError                        <- 1. la excepción de Requests
HTTPSConnectionPool(host='example.com', port=443):    <- 2. el pool: host y puerto de destino
Max retries exceeded with url: /                      <- 3. el texto envoltorio y la ruta
(Caused by ProxyError('Unable to connect to proxy',   <- 4. la causa real, la más externa primero
    OSError('Tunnel connection failed: 403 Forbidden')))
```

La parte 2 es donde la gente se equivoca. Con una URL `https://`, la línea del pool muestra el **destino**, aunque la petición pase por un proxy; la dirección del proxy solo aparece dentro de la parte 4, si es que aparece. Con una URL `http://` normal enviada a través de un proxy ocurre lo contrario: la línea del pool muestra el proxy y la parte 3 lleva la URL de destino completa, como en `HTTPConnectionPool(host='127.0.0.1', port=8083): Max retries exceeded with url: http://example.com/`.

## ¿Por qué dice «max retries» si nunca configuraste reintentos?

Requests no reintenta las conexiones fallidas por defecto. En el código fuente de `requests/adapters.py`, `DEFAULT_RETRIES = 0`, y el adaptador lo convierte en `Retry(0, read=False)`. La documentación de urllib3 sobre la [clase Retry](https://urllib3.readthedocs.io/en/stable/reference/urllib3.util.html) indica que los errores se envuelven en `MaxRetryError` salvo que los reintentos se desactiven con `retries=False`. Cero reintentos no cuenta como desactivado, así que un solo intento fallido ya sale como «Max retries exceeded».

Subir el número de reintentos no arregla la causa. Un nombre de proxy mal escrito, un puerto equivocado o un túnel rechazado fallan igual cada vez. En nuestra prueba, una dirección inalcanzable con un tiempo de espera de conexión de 2 segundos falló a los 2 segundos con la configuración por defecto y a los 7 segundos con dos reintentos y un backoff corto. Los reintentos solo ayudan con cortes breves de red.

## ¿Cómo se lee el mensaje de error paso a paso?

Lee el mensaje de fuera hacia dentro:

1. **Busca la excepción de Requests.** `ProxyError` significa que el fallo ocurrió de camino al proxy o en el propio proxy.
2. **Lee la línea del pool.** Con destinos `https://`, `host` y `port` son el destino. El puerto 443 a través de un proxy HTTP significa que la petición viaja por un túnel CONNECT.
3. **Lee la primera clase después de `Caused by`.** Es el diagnóstico de urllib3: `NameResolutionError`, `NewConnectionError`, `ConnectTimeoutError`, `SSLError` o `ProxyError`.
4. **Lee el texto más interno.** `Failed to resolve 'name'`, `[Errno 111] Connection refused` en Linux, `[WinError 10061]` en Windows, `CERTIFICATE_VERIFY_FAILED` o `Tunnel connection failed: 403 Forbidden`. Un `host=` en esta parte puede ser el proxy.
5. **Fíjate en cuánto tardó la llamada.** Un fallo justo al cumplirse tu tiempo de espera de conexión (o un múltiplo de él) es un timeout; uno más rápido es un rechazo o un túnel denegado. En Windows, una conexión rechazada tardó unos dos segundos por dirección en nuestras pruebas, y `localhost` tardó cuatro (primero IPv6, luego IPv4).

## Errores de HTTPSConnectionPool: qué te dice la parte Caused by

Generamos todas las cadenas de abajo con Python 3.13, Requests 2.34.2 y urllib3 2.8.0, a través de un proxy de prueba local. Las partes largas se acortan con `...`; el texto de error de Windows depende además del idioma del sistema.

| Caused by (parte más interna) | Excepción de Requests | Qué pasó | Qué revisar primero |
|---|---|---|---|
| `NameResolutionError(... Failed to resolve 'no-such-host.invalid' ...)` | `ConnectionError` | El nombre del destino no se resolvió | Ortografía de la URL, DNS, VPN |
| `NewConnectionError(... [Errno 111] o [WinError 10061] ...)` | `ConnectionError` | Nada escucha en ese puerto | Que el servicio esté activo, el puerto correcto |
| `ConnectTimeoutError(... 'Connection to 10.255.255.1 timed out. (connect timeout=2)')` | `ConnectTimeout` | No hubo conexión TCP dentro del tiempo de espera | Dirección, firewall, valor del tiempo de espera |
| `SSLError(SSLCertVerificationError(... CERTIFICATE_VERIFY_FAILED ...))` | `SSLError` | El certificado no es de confianza | certifi, certificado raíz de la empresa |
| `ProxyError('Unable to connect to proxy', NewConnectionError(...))` | `ProxyError` | El puerto del proxy rechazó la conexión | Host y puerto del proxy, firewall local |
| `ProxyError('Unable to connect to proxy', NameResolutionError(... 'pr.proxynet.invalid' ...))` | `ProxyError` | El nombre del proxy no se resolvió | Errata en el host del proxy |
| `ProxyError('Unable to connect to proxy', ConnectTimeoutError(...))` | `ProxyError` | El proxy no respondió a tiempo | Dirección del proxy, reglas de salida para ese puerto |
| `ProxyError(..., OSError('Tunnel connection failed: 403 Forbidden'))` | `ProxyError` | El proxy rechazó el túnel CONNECT | Puerto de destino, tipo y puerto del proxy, reglas de permiso |
| `ProxyError(..., OSError('Tunnel connection failed: 407 Proxy Authentication Required'))` | `ProxyError` | El proxy quiere credenciales válidas | Consulta [Autenticación de proxy](/es/blog/proxy-authentication-methods) |
| `ProxyError(..., OSError('Tunnel connection failed: 502 Bad Gateway'))` | `ProxyError` | El proxy no pudo llegar al destino | Host y puerto de destino |
| `ProxyError('... Your proxy appears to only use HTTP and not HTTPS ...', SSLError(... WRONG_VERSION_NUMBER ...))` | `ProxyError` | La URL del proxy empieza por `https://` | Escribe `http://` |

Tres detalles quedan fuera de la tabla. Un proxy que agota el tiempo de espera lanza `ProxyError`, así que `except ConnectTimeout` nunca lo ve. Un timeout de lectura no se envuelve: llega como `ReadTimeout` con el texto `Read timed out. (read timeout=3)`. Y una URL `http://` normal a través de un proxy con una contraseña incorrecta no lanza nada; recibes una respuesta con estado `407`.

## ¿Qué significa «ProxyError: Cannot connect to proxy»?

`Cannot connect to proxy.` (urllib3 1.26) y `Unable to connect to proxy` (urllib3 2.x) son el mismo error. Los dos pueden aparecer en el mismo equipo: pip 25.x incluye urllib3 1.26.20, mientras que una instalación nueva de Requests trae urllib3 2.8.0. Lee el segundo argumento, no la redacción:

- **`NewConnectionError`**: nada escucha en ese puerto del proxy, o un firewall local lo bloquea. Vuelve a copiar el host y el puerto desde el panel.
- **`NameResolutionError`**: el nombre de host del proxy está mal escrito o tu DNS no puede resolverlo.
- **`ConnectionResetError`** (`Errno 104` en Linux, `WinError 10054` en Windows): el proxy o un dispositivo intermedio cerró la conexión.
- **`OSError('Tunnel connection failed: ...')`**: el proxy respondió pero no abrió el túnel; el código de estado es su respuesta.

Si el problema aparece en el navegador como «El servidor proxy no responde», lo explicamos en [qué es un error de proxy y cómo solucionarlo](/es/blog/proxy-server-not-responding).

## Tunnel connection failed: 403 Forbidden: ¿por qué el proxy rechaza CONNECT?

Una petición `https://` a través de un proxy HTTP empieza con `CONNECT example.com:443`. El proxy abre una conexión TCP con el destino y reenvía bytes cifrados; nunca ve la página. Según [RFC 9110, sección 9.3.6](https://www.rfc-editor.org/rfc/rfc9110.html#name-connect), cualquier respuesta distinta de un 2xx significa que el túnel no se estableció. El módulo `http.client` de Python lee esa respuesta y escribe `Tunnel connection failed`.

El `403` es una decisión del proxy; el destino nunca vio tu petición. El RFC dice que los proxies deberían limitar CONNECT a puertos conocidos o a una lista de destinos seguros, porque un túnel al puerto 25 podría reenviar spam. Motivos habituales:

- **El puerto de destino no está permitido.** `https://example.com:8443/` puede fallar mientras `https://example.com/` funciona.
- **Una lista de permitidos.** Los proxies corporativos y algunas plataformas de hosting solo permiten dominios aprobados. Pregunta al equipo de TI o lee la documentación de la plataforma; no esquives la política.
- **El tipo o el puerto de proxy equivocado**, por ejemplo un puerto pensado para otro protocolo.
- **Reglas del proveedor.** Un proveedor puede rechazar túneles hacia ciertos destinos; su documentación indica qué códigos de estado usa.

Un `403` del sitio de destino llega como una respuesta normal con una página; ese caso está en [Códigos de estado HTTP en web scraping](/es/blog/http-status-codes-web-scraping). Un `407` en el túnel significa que el proxy pide credenciales.

## ¿La URL del proxy debe empezar por http:// o por https://?

En el diccionario `proxies`, la clave es el esquema del **destino** y el valor es cómo llegar al **proxy**. El propio ejemplo de la documentación de Requests asigna `'https'` a `'http://10.10.1.10:1080'`. La mayoría de los proxies hablan HTTP simple y abren un túnel CONNECT para los sitios cifrados (consulta nuestra página de [proxy HTTPS](/es/https-proxy)), así que las dos claves llevan un valor `http://`:

```python
proxies = {
    "http": "http://user:pass@pr.proxynet.io:8000",
    "https": "http://user:pass@pr.proxynet.io:8000",  # aquí también http://
}
```

Con `https://` en el valor, urllib3 2.x abre TLS con el propio proxy, un proxy HTTP simple responde con bytes que no son TLS y recibes `WRONG_VERSION_NUMBER` con la pista `Your proxy appears to only use HTTP and not HTTPS`. La página de urllib3 sobre [este error](https://urllib3.readthedocs.io/en/latest/advanced-usage.html#https-proxy-error-http-proxy) da la misma solución, también cuando el problema es una variable `HTTPS_PROXY` mal definida.

Dos puntos relacionados:

- **SOCKS.** Instala `requests[socks]`. Con `socks5://` es tu equipo quien resuelve el nombre, así que en nuestra prueba un nombre erróneo falló antes de contactar con el proxy; con `socks5h://` lo resuelve el proxy ([Diferencia entre SOCKS y HTTP](/es/blog/socks-vs-http-proxy), [proxy SOCKS5](/es/socks5-proxy)).
- **Variables de entorno.** La [página de uso avanzado de Requests](https://requests.readthedocs.io/en/latest/user/advanced/) advierte que los proxies del entorno pueden sobrescribir `session.proxies` y recomienda `proxies=` en cada petición. Las variables se explican en [Proxy con wget](/es/blog/wget-proxy).

## ¿Qué pasa sin tiempo de espera? ConnectTimeout frente a ReadTimeout

Requests no tiene tiempo de espera por defecto: sin `timeout=`, una llamada puede quedarse colgada durante minutos sin darte ningún error que leer. Pasa una tupla como `timeout=(3.05, 20)`: primero el tiempo de espera de conexión y después el de lectura, en segundos. La documentación sugiere un tiempo de espera de conexión algo superior a un múltiplo de 3, la ventana de retransmisión TCP por defecto.

Los dos tiempos de espera fallan de forma distinta:

- **ConnectTimeout**: no hubo conexión TCP a tiempo. Llega envuelto en «Max retries exceeded», y la referencia de la API lo considera seguro de reintentar.
- **ReadTimeout**: hubo conexión, pero no llegaron datos dentro del tiempo de espera de lectura. No se envuelve, y puede que el servidor ya haya procesado la petición, así que reintentar un POST puede hacer el trabajo dos veces.

El tiempo de espera de conexión se aplica a cada dirección IP, así que un nombre con direcciones IPv4 e IPv6 puede duplicar la espera. El tiempo de espera de lectura es el intervalo entre bytes, no un límite para toda la descarga. Los valores por defecto de otras bibliotecas están en [HTTPX, Requests y AIOHTTP: comparativa](/es/blog/httpx-vs-requests-vs-aiohttp).

## El mismo error en pip: «Retrying ... after connection broken by»

pip incluye sus propias copias de Requests y urllib3, así que `pip install` falla por las mismas razones. La [documentación de pip](https://pip.pypa.io/en/stable/cli/pip/) recoge los valores por defecto: `--retries 5` y `--timeout 15` segundos. Ejecutamos pip 26.2.1 con `--retries 2` a través de un proxy local que rechaza todos los CONNECT con `403`:

```text
WARNING: Retrying (Retry(total=1, connect=None, read=None, redirect=None, status=None)) after connection broken by 'OSError('Tunnel connection failed: 403 Forbidden')': /simple/six/
WARNING: Retrying (Retry(total=0, connect=None, read=None, redirect=None, status=None)) after connection broken by 'OSError('Tunnel connection failed: 403 Forbidden')': /simple/six/
ERROR: Could not find a version that satisfies the requirement six (from versions: none)
ERROR: No matching distribution found for six
```

El paquete existe; pip nunca llegó al índice. La causa está en las líneas `WARNING`. pip 25.2 mostraba el mismo aviso con la redacción de urllib3 1.26, `ProxyError('Cannot connect to proxy.', ...)`.

Para `CERTIFICATE_VERIFY_FAILED` detrás de un proxy corporativo que inspecciona TLS, pasa el certificado raíz de la empresa con `--cert`; `--trusted-host` desactiva la verificación y es el último recurso. La configuración del proxy en pip se explica en [Configurar proxy en Linux](/es/blog/linux-proxy-settings).

## Un script de Python que nombra la causa

El script convierte una excepción en una sola línea: qué falló y qué revisar. En concreto:

- reintenta solo las conexiones fallidas, dos veces;
- mantiene `read=False`: en nuestra prueba, `read=0` convertía un `ReadTimeout` en un `ConnectionError` envuelto;
- fija `other=0`: sin eso, un túnel rechazado con `403` o `407` se intentaba tres veces;
- pasa `proxies=` y `timeout=(3.05, 20)` en cada petición;
- captura `ProxyError`, `SSLError` y `ConnectTimeout` antes que `ConnectionError`, su clase padre.

Los reintentos por código de estado son otra capa ([Códigos de estado HTTP en web scraping](/es/blog/http-status-codes-web-scraping)), y la rotación también ([Cómo rotar proxies en Python](/es/blog/how-to-rotate-proxies-in-python)).

```python
"""Encuentra la causa real detrás de "Max retries exceeded with url" y dice qué revisar."""
import re
import ssl
import time

import requests
from requests.adapters import HTTPAdapter
from urllib3.exceptions import (
    ConnectTimeoutError,
    NameResolutionError,
    NewConnectionError,
    ProxyError as Urllib3ProxyError,
)
from urllib3.util import Retry

PROXY = "http://user:pass@pr.proxynet.io:8000"  # http:// incluso para destinos https://
TIMEOUT = (3.05, 20)  # segundos: (conexión, lectura)

def make_session():
    """Una sesión que reintenta las conexiones fallidas dos veces y nada más."""
    retry = Retry(
        total=2,
        connect=2,      # fallos de DNS, conexiones rechazadas y conexiones agotadas por tiempo
        read=False,     # que ReadTimeout siga siendo ReadTimeout: el servidor puede tener ya la petición
        other=0,        # un proxy que rechazó el túnel lo volverá a rechazar
        status=0,       # los reintentos por código de estado son otra capa
        backoff_factor=0.5,
    )
    adapter = HTTPAdapter(max_retries=retry)
    session = requests.Session()
    session.mount("http://", adapter)
    session.mount("https://", adapter)
    return session

def causes(exc):
    """La excepción y cada error envuelto dentro de ella, del más externo al más interno."""
    chain = []
    while exc is not None and all(exc is not seen for seen in chain):
        chain.append(exc)
        inner = None
        for candidate in (getattr(exc, "reason", None), getattr(exc, "original_error", None),
                          exc.__cause__, exc.__context__, *exc.args[:2]):
            if isinstance(candidate, BaseException):
                inner = candidate
                break
        exc = inner
    return chain

def explain(exc):
    """Una línea: qué falló, en qué lado y qué revisar primero."""
    chain = causes(exc)
    text = " | ".join(str(e) for e in chain)
    side = "proxy" if any(isinstance(e, Urllib3ProxyError) for e in chain) else "target"

    tunnel = re.search(r"Tunnel connection failed: (\d{3})", text)
    if tunnel:
        code = tunnel.group(1)
        if code == "407":
            return "proxy asked for credentials (407): check user:pass or the IP whitelist"
        if code == "403":
            return "proxy refused the CONNECT tunnel (403): check the target port and the proxy's allow rules"
        return f"proxy was reached but could not reach the target ({code}): check the target host and port"
    if "appears to only use HTTP" in text:
        return "proxy URL starts with https:// but the proxy speaks plain HTTP: write http://"
    if any(isinstance(e, NameResolutionError) for e in chain):
        host = re.search(r"Failed to resolve '([^']+)'", text)
        return f"{side} name {host.group(1) if host else ''} does not resolve: check spelling, DNS and VPN"
    if any(isinstance(e, ssl.SSLCertVerificationError) for e in chain):
        return "certificate not trusted: update certifi or set REQUESTS_CA_BUNDLE to your company root CA"
    if isinstance(exc, requests.exceptions.ReadTimeout):
        return "connected, but no answer within the read timeout: the server may have the request"
    if any(isinstance(e, NewConnectionError) for e in chain):
        return f"{side} refused the connection: wrong port, service down, or a firewall rejects it"
    if any(isinstance(e, ConnectTimeoutError) for e in chain):
        return f"no TCP connection to the {side} within the connect timeout: check address, port, firewall"
    return f"unrecognised, read the innermost error: {chain[-1]!r}"

def fetch(session, url, proxy=PROXY):
    """Hace GET a una URL e imprime el estado, o el diagnóstico si la petición falló."""
    proxies = {"http": proxy, "https": proxy} if proxy else None  # por petición: las variables de entorno no pueden sobrescribirlo
    start = time.monotonic()
    try:
        resp = session.get(url, proxies=proxies, timeout=TIMEOUT)
    except requests.exceptions.ProxyError as exc:      # antes que ConnectionError: es una subclase suya
        kind, error = "ProxyError", exc
    except requests.exceptions.SSLError as exc:        # también es un ConnectionError
        kind, error = "SSLError", exc
    except requests.exceptions.ConnectTimeout as exc:  # es ConnectionError y Timeout a la vez
        kind, error = "ConnectTimeout", exc
    except requests.exceptions.ReadTimeout as exc:     # nunca se envuelve en "Max retries exceeded"
        kind, error = "ReadTimeout", exc
    except requests.exceptions.ConnectionError as exc:
        kind, error = "ConnectionError", exc
    else:
        print(f"{'OK':<16}{time.monotonic() - start:6.2f}s  {url}  HTTP {resp.status_code}")
        return resp
    print(f"{kind:<16}{time.monotonic() - start:6.2f}s  {url}\n{'':<24}{explain(error)}")
    return None

if __name__ == "__main__":
    session = make_session()
    for url in ["https://httpbin.org/ip", "https://example.com/"]:
        fetch(session, url)
```

En urllib3 2.x, `NameResolutionError` es una subclase de `NewConnectionError`, que a su vez es subclase de `ConnectTimeoutError`, así que `explain()` comprueba primero la clase más específica. El script solo necesita `pip install requests`.

### Así se ve la salida

Ejecutamos `fetch()` en Windows 11 una vez por caso: un proxy de prueba local (contraseña correcta e incorrecta, puerto cerrado, nombre mal escrito, esquema `https://`), un segundo proxy que rechaza todos los CONNECT y hosts reales para los casos de certificado y de tiempo de espera. El tiempo de espera de lectura era de 5 segundos:

```text
OK                0.77s  https://httpbin.org/ip  HTTP 200
ProxyError        0.02s  https://httpbin.org/ip
                        proxy asked for credentials (407): check user:pass or the IP whitelist
ProxyError        0.00s  https://example.com/
                        proxy refused the CONNECT tunnel (403): check the target port and the proxy's allow rules
ProxyError        0.02s  https://no-such-host.invalid/
                        proxy was reached but could not reach the target (502): check the target host and port
ProxyError        7.10s  https://httpbin.org/ip
                        proxy refused the connection: wrong port, service down, or a firewall rejects it
ProxyError        1.02s  https://httpbin.org/ip
                        proxy name pr.proxynet.invalid does not resolve: check spelling, DNS and VPN
ProxyError        0.21s  https://httpbin.org/ip
                        proxy URL starts with https:// but the proxy speaks plain HTTP: write http://
SSLError          0.63s  https://self-signed.badssl.com/
                        certificate not trusted: update certifi or set REQUESTS_CA_BUNDLE to your company root CA
ConnectTimeout   10.16s  http://10.255.255.1/
                        no TCP connection to the target within the connect timeout: check address, port, firewall
ReadTimeout       5.02s  http://127.0.0.1:8082/
                        connected, but no answer within the read timeout: the server may have the request
ConnectionError  13.12s  http://localhost:8083/
                        target refused the connection: wrong port, service down, or a firewall rejects it
```

Los túneles rechazados fallaron en milisegundos porque `other=0` los detuvo. El puerto cerrado del proxy necesitó tres intentos de unos dos segundos más un segundo de backoff; el tiempo de espera de conexión, tres veces 3,05 segundos más el backoff; y `localhost`, 13 segundos, porque cada intento probó IPv6 y luego IPv4. El `502` vino de nuestro proxy de prueba, que no pudo resolver el destino.

## Dónde aparece este error

- **Scraping a través de un proxy:** los errores del proxy aparecen aquí antes que cualquier código de estado ([extracción de datos](/es/data-scraping)).
- **Builds de CI y Docker detrás de un proxy corporativo:** los builds a menudo no reciben la configuración de proxy del host, y `localhost` es el contenedor ([Configurar proxy en Linux](/es/blog/linux-proxy-settings)).
- **Integraciones de API con una IP de salida fija:** un túnel rechazado parece una caída de la API ([IP estática para API](/es/blog/static-ip-for-api-access)).
- **Probar un proxy nuevo:** lanza una sola petición con tiempo de espera antes de un trabajo completo ([Cómo probar un proxy](/es/blog/how-to-test-a-proxy)).
- **Automatización de navegadores:** el mismo fallo del túnel aparece como `ERR_TUNNEL_CONNECTION_FAILED` ([Playwright con un proxy](/es/blog/playwright-proxy)).
- **Redes corporativas:** tanto un firewall como un proxy filtran conexiones ([Proxy o firewall](/es/blog/proxy-vs-firewall)).

## Errores comunes

- **Subir el número de reintentos.** Una causa permanente sigue siendo permanente; solo esperas más.
- **Culpar al destino porque su nombre está en la línea del pool.** Con URLs `https://`, el proxy solo aparece entre paréntesis.
- **Olvidar una variable `HTTPS_PROXY` antigua.** Puede sustituir `session.proxies` sin avisar.
- **Dejar `verify=False` en producción.** La documentación de Requests advierte que te expone a ataques de intermediario (man-in-the-middle). Actualiza certifi o define `REQUESTS_CA_BUNDLE`; para un proxy local de depuración, consulta [Proxy MITM](/es/blog/mitm-proxy).
- **Capturar `ConnectionError` primero.** Se traga `ProxyError`, `SSLError` y `ConnectTimeout`.
- **Confundir `403` y `407` en el túnel.** Uno es una regla; el otro, credenciales.
- **Leer solo la última línea de pip.** La causa está en las líneas `WARNING` de encima.

## Guía de decisión

| Lo que ves | Qué hacer |
|---|---|
| `NameResolutionError` en Caused by | Corrige el nombre que falló (host del proxy o URL); sin reintentos |
| `ProxyError` con `NewConnectionError` | Vuelve a copiar el host y el puerto del proxy; revisa el firewall local |
| `Tunnel connection failed: 403` | Revisa el puerto de destino, el tipo y el puerto del proxy y las reglas de permiso |
| `Tunnel connection failed: 407` | Revisa las credenciales o la lista blanca de IP |
| `SSLError: CERTIFICATE_VERIFY_FAILED` | Actualiza certifi o define `REQUESTS_CA_BUNDLE` (pip: `--cert`) |
| Las llamadas se cuelgan o agotan el tiempo de vez en cuando | `timeout=(3.05, 20)` más un `Retry` solo para conexiones |
| pip dice «No matching distribution found» | Lee la línea `WARNING: Retrying` |

## Preguntas frecuentes

### ¿Subir el número de reintentos arregla «Max retries exceeded»?

Solo cuando la red se cae un momento. Con un nombre que no se resuelve, un puerto cerrado o un túnel rechazado, cada reintento falla igual. Lee primero la parte `Caused by`.

### ¿Cuál es el tiempo de espera por defecto en Python Requests?

No hay ninguno: sin `timeout=`, una petición puede esperar indefinidamente. Da a cada llamada una tupla `(connect, read)`.

### ¿El timeout de Requests va en segundos o en milisegundos?

En segundos. Sirve un float como `3.05`; un solo número fija las dos fases, y una tupla como `(3.05, 20)` las fija por separado.

### ¿Puedo usar verify=False para CERTIFICATE_VERIFY_FAILED?

Solo para una prueba local rápida, porque permite que cualquiera en el camino lea o modifique el tráfico. La solución duradera es un certifi actualizado o, detrás de un proxy corporativo que inspecciona TLS, su certificado raíz en `REQUESTS_CA_BUNDLE`.

### ¿Por qué pip dice «No matching distribution found» si el paquete existe?

pip no pudo llegar al índice de paquetes, así que no encontró ninguna versión. Las líneas `WARNING: Retrying ... after connection broken by` de encima nombran la causa real, como `Tunnel connection failed: 403 Forbidden`.

### ¿Por qué recibo este error al llamar a localhost:8000?

Nada escucha en ese puerto: el servidor está caído, usa otro puerto o tu código se ejecuta en Docker, donde `localhost` es el contenedor. El error más interno es `[Errno 111]` o `[WinError 10061]`. Cómo averiguar qué programa ocupa un puerto lo explicamos en [¿Qué es el puerto 8080?](/es/blog/port-8080)

## En resumen

«Max retries exceeded with url» es un envoltorio: la causa está después de `Caused by`, y aparece tras un solo intento porque Requests no reintenta por defecto. En los errores de proxy, distingue un fallo de camino al proxy de un túnel que el proxy rechazó, y mantén `http://` en la URL del proxy. Da a cada petición una tupla de tiempo de espera y reintenta solo los fallos de conexión. Prueba una configuración nueva primero con una sola petición ([cómo probar un proxy](/es/blog/how-to-test-a-proxy)) y compara opciones en nuestra página de [servicios de proxy](/es/proxy).
