Análisis8 min de lectura

Qué medir en un benchmark de proxies

Compara proxies por éxito de contenido, tiempos de respuesta y comportamiento de sesión, con un harness Python basado en curl que registra fallos y calcula resultados.

En esta página

Compara proxies por la tasa de éxito de la aplicación, las distribuciones de tiempos de respuesta, el establecimiento de conexión y el comportamiento de sesión. Define el éxito para cada destino antes de recoger resultados. Los ejemplos de abajo incluyen una línea base directa observada y un pequeño harness que comprueba un marcador de respuesta y calcula la tasa de éxito y la latencia. Todavía no hemos publicado resultados comparables de proxies residenciales y de datacenter.

Define el éxito antes de medir la velocidad

Unos tiempos de respuesta bajos pueden ocultar un coste alto por petición exitosa cuando los fallos disparan reintentos. Cuenta el trabajo completado además del tiempo transcurrido, e incluye cualquier tráfico de reintentos en tu estimación de coste.

El éxito necesita una definición por destino: el estado esperado, el contenido requerido y un resultado de redirección aceptable. Una respuesta HTTP 200 que contenga un desafío CAPTCHA puede incumplir esa definición. Un campo ausente o una descarga incompleta también pueden incumplirla. Un marcador de texto simple sirve para una comprobación inicial; una carga de trabajo de producción puede necesitar registros parseados, validación de esquema o un flujo completo de varios pasos.

Registra las respuestas exitosas y cada categoría de fallo por separado, y después investiga la causa. Un tiempo de espera puede agotarse en varias partes del camino. Un 403 por sí solo no identifica qué intermediario u origen aplicó una restricción. Ni el tiempo de respuesta ni el estado bastan para atribuir la culpa.

Registra el tiempo hasta el primer byte y el tiempo total

curl reporta hitos de tiempo medidos desde el inicio de la transferencia. El tiempo hasta el primer byte (TTFB) te dice cuándo empezó la respuesta; el tiempo total incluye la recepción de su cuerpo. Registra ambos, porque un primer byte rápido puede preceder igualmente a una descarga lenta o incompleta.

El 11 de septiembre de 2026, tres peticiones directas consecutivas a https://example.com/ desde un VPS en Helsinki con curl 8.18 produjeron los siguientes valores. No se usó ningún proxy. Las etiquetas de abajo hacen explícito el significado acumulativo:

code
http=200 tcp_complete=0.009780s tls_complete=0.023224s ttfb=0.044821s total=0.044983s
http=200 tcp_complete=0.008289s tls_complete=0.023851s ttfb=0.039902s total=0.039966s
http=200 tcp_complete=0.008149s tls_complete=0.031935s ttfb=0.061614s total=0.061688s

Eso es una línea base observada de aproximadamente 40–62 ms hasta el primer byte, no un límite inferior para futuras peticiones. Tres observaciones no establecen una distribución estable. Recoge una línea base nueva en la misma máquina y destino cerca de la comparación de proxies, y conserva la fecha de ejecución, la versión del cliente y la configuración.

Este comando acotado recoge los mismos campos para una nueva petición directa:

sh
curl --disable --silent --show-error --noproxy '*' \
  --connect-timeout 5 --max-time 20 --retry 0 \
  --output /dev/null \
  --write-out 'http=%{http_code} tcp_complete=%{time_connect}s tls_complete=%{time_appconnect}s ttfb=%{time_starttransfer}s total=%{time_total}s exit=%{exitcode}\n' \
  'https://example.com/'

Para la misma petición a través de un proxy HTTP(S), usa curl 8.3 o posterior. Asigna a PROXY_URL el gateway sin credenciales del proveedor, incluidos su esquema y puerto soportados. Haz que tu gestor de secretos proporcione PROXY_USERNAME y PROXY_PASSWORD en el entorno. Este ejemplo sigue la guía de configuración de curl: curl importa las credenciales por sí mismo, de modo que la contraseña expandida no es un argumento del shell. No actives el trazado del shell.

sh
: "${PROXY_URL:?Set a credential-free HTTP(S) gateway URL}"
curl --disable --silent --show-error --noproxy '' \
  --proxy "$PROXY_URL" --proxy-basic \
  --variable %PROXY_USERNAME --variable %PROXY_PASSWORD \
  --expand-proxy-user '{{PROXY_USERNAME}}:{{PROXY_PASSWORD}}' \
  --connect-timeout 5 --max-time 20 --retry 0 \
  --output /dev/null \
  --write-out 'http=%{http_code} tunnel=%{http_connect} tcp_complete=%{time_connect}s tls_complete=%{time_appconnect}s ttfb=%{time_starttransfer}s total=%{time_total}s exit=%{exitcode}\n' \
  'https://example.com/'

--disable va primero para ignorar la configuración personal de curl. El --noproxy '*' del comando directo evita los proxies heredados; la lista de exclusión vacía del comando con proxy impide que NO_PROXY excluya el destino. Estos comandos usan una conexión nueva por cada proceso de curl, no siguen redirecciones y no reintentan. Un error HTTP puede seguir teniendo código de salida cero en curl porque los comandos omiten deliberadamente --fail; inspecciona el estado HTTP por separado. Consulta el manual de curl para la selección de ruta y la guía de tiempos de espera para los plazos.

Entiende qué incluyen los tiempos de conexión

Para un destino HTTPS a través de un proxy HTTP, curl establece una conexión con el proxy, solicita un túnel CONNECT y después negocia TLS con el destino a través de ese túnel. La RFC 9110 define CONNECT.

El intervalo entre time_connect y time_appconnect incluye el establecimiento del túnel y de TLS. Restar el intervalo directo correspondiente no aísla la sobrecarga del CONNECT: el enrutamiento y las condiciones del handshake también cambian. Un proxy HTTPS añade otro handshake TLS. Un objeto estático de CDN no elimina esas diferencias.

Usa los hitos para localizar dónde aparece el retraso, y después investiga con endpoints controlados o con registros del cliente y del proxy adecuadamente instrumentados. Resta solo hitos que se completaron en la misma transferencia simple. Las peticiones fallidas, las conexiones reutilizadas y las redirecciones necesitan una interpretación aparte; un valor cero puede significar que una fase no se alcanzó.

%{http_connect} reporta el código de respuesta del CONNECT HTTP por separado del estado HTTP de la transferencia. --proxy-header envía una cabecera al proxy; no reporta el estado del túnel. Prefiere campos seleccionados de estado y de tiempo antes que trazas detalladas que pueden contener datos de autenticación. Estas interpretaciones siguen los campos de tiempo documentados de curl.

Comprueba la rotación frente a la política prometida

La IP de salida es la dirección pública que observa el destino. Consulta un endpoint de diagnóstico soportado por el proveedor o un endpoint que controles y que reporte la dirección del llamador. ipify es una opción pública; revisa sus indicaciones de uso vigentes antes de automatizar peticiones.

Conserva una secuencia de resultados con marcas de tiempo antes de reducirla a recuentos de direcciones únicas. Para cada intento, registra la región seleccionada, el ajuste de sesión, el resultado de transporte y una dirección IPv4 o IPv6 validada. Excluye las peticiones fallidas y los cuerpos inválidos de los recuentos de direcciones, reportando esos fallos por separado. En Python, ipaddress.ip_address(value.strip()) puede validar una respuesta de dirección en texto plano.

Compara esa secuencia con la política de rotación del proveedor. La rotación puede ocurrir por petición, por conexión o por intervalo de tiempo, y el pool puede reutilizar una dirección. Diez peticiones no exigen diez salidas distintas a menos que el producto lo prometa explícitamente. Una sesión persistente (sticky) está pensada para conservar una salida durante su duración documentada; comprueba su expiración y su comportamiento ante desconexiones antes de tratar una reasignación como un fallo.

Usa los ajustes de país o región y de sesión que tu carga de trabajo necesite. Repite la prueba a lo largo de la ventana de sesión correspondiente. La guía de proxies rotativos vs persistentes explica qué comportamientos evaluar. El benchmark de contenido de abajo no mide la rotación.

Elige una muestra y una ventana de observación

Diez peticiones pueden descubrir fallos básicos, pero ofrecen poca evidencia sobre una distribución de latencia. Las peticiones lentas pueden bloquear una cola; una media por sí sola puede ocultar valores atípicos lentos.

Nuestro punto de partida propuesto es 200 peticiones por destino y configuración, repartidas durante al menos una hora. Es una elección de diseño de prueba, no un umbral universal de fiabilidad. El percentil 95 de una muestra pequeña depende de muy pocas observaciones. Reporta el tamaño de la muestra, los recuentos de fallos y la ventana de observación, y después repite en los momentos y condiciones relevantes para la carga de trabajo.

Al comparar proveedores, intercala sus peticiones durante la misma ventana. Esto reduce las diferencias causadas por cambios en la carga del destino sin garantizar condiciones idénticas. Mantén constantes el contenido de la petición, los ajustes del cliente y los criterios de éxito. Prueba la reutilización de conexiones y la concurrencia de producción por separado del ejemplo secuencial con procesos nuevos de abajo.

Un pequeño benchmark de contenido con resumen

Guárdalo como proxy-bench.py y ejecuta python3 proxy-bench.py. Requiere Python 3 y curl 8.3+. Usa las mismas variables de entorno privadas del proxy que arriba. Define BENCH_MODE=direct para una ejecución directa o déjalo en proxy. Cada ejecución crea un nuevo directorio de resultados e imprime su ruta.

El destino por defecto es una pequeña página ilustrativa. Para una comparación real, asigna a BENCH_URL un endpoint HTTPS permitido y sustituye BENCH_MARKER por una cadena esperada significativa. No uses URL que contengan credenciales o parámetros sensibles. Adapta la comprobación de contenido a tu formato de respuesta real antes de citar el éxito de la aplicación.

python
import collections
import datetime
import json
import math
import os
from pathlib import Path
import statistics
import subprocess
import tempfile
import time

url = os.environ.get("BENCH_URL", "https://example.com/")
marker = os.environ.get("BENCH_MARKER", "<h1>Example Domain</h1>").encode()
mode = os.environ.get("BENCH_MODE", "proxy")
if not url.startswith("https://") or not marker or mode not in {"direct", "proxy"}:
    raise SystemExit("Use an HTTPS URL, a nonempty marker and direct or proxy mode")
route = ["--noproxy", "*"]
if mode == "proxy":
    route = ["--noproxy", "", "--proxy", os.environ["PROXY_URL"], "--proxy-basic",
             "--variable", "%PROXY_USERNAME", "--variable", "%PROXY_PASSWORD",
             "--expand-proxy-user", "{{PROXY_USERNAME}}:{{PROXY_PASSWORD}}"]
fields = ["http", "tunnel", "tcp_complete", "tls_complete", "ttfb", "total"]
write_out = "%{http_code} %{http_connect} %{time_connect} %{time_appconnect} %{time_starttransfer} %{time_total}"
count, window = 200, 3600.0
directory = Path(tempfile.mkdtemp(prefix="proxy-bench-", dir="."))
rows = []
started = time.monotonic()
with (directory / "results.jsonl").open("x") as log:
    for i in range(count):
        due = started + i * window / (count - 1)
        time.sleep(max(0, due - time.monotonic()))
        timestamp = datetime.datetime.now(datetime.timezone.utc).isoformat()
        with tempfile.TemporaryDirectory() as scratch:
            body = Path(scratch) / "body"
            result = subprocess.run(
                ["curl", "--disable", "--silent", "--connect-timeout", "5",
                 "--max-time", "20", "--retry", "0", *route,
                 "--output", str(body), "--write-out", write_out, "--url", url],
                capture_output=True, text=True)
            values = result.stdout.split()
            timings = dict(zip(fields, values)) if len(values) == len(fields) else {}
            if result.returncode:
                outcome = f"curl_{result.returncode}"
            elif not timings:
                outcome = "missing_measurements"
            elif timings["http"] != "200":
                outcome = "http_" + timings["http"]
            elif not body.exists() or marker not in body.read_bytes():
                outcome = "content_mismatch"
            else:
                outcome = "success"
        row = dict(attempt=i + 1, timestamp=timestamp, mode=mode,
                   exit_code=result.returncode, outcome=outcome, **timings)
        rows.append(row)
        log.write(json.dumps(row) + "\n")
        log.flush()

successful = [row for row in rows if row["outcome"] == "success"]
summary = {"attempts": len(rows), "successes": len(successful),
           "success_rate": len(successful) / len(rows),
           "outcomes": dict(collections.Counter(row["outcome"] for row in rows))}
for metric in ("ttfb", "total"):
    samples = sorted(float(row[metric]) for row in successful)
    summary[metric] = ({"median_seconds": statistics.median(samples),
                        "p95_seconds": samples[math.ceil(0.95 * len(samples)) - 1]}
                       if samples else None)
(directory / "summary.json").write_text(json.dumps(summary, indent=2) + "\n")
print(directory)
print(json.dumps(summary, indent=2))

La primera petición empieza de inmediato; la número 200 se programa 3.600 segundos después de la primera. Las peticiones siguen siendo secuenciales. Si una petición tarda más que el espaciado, los inicios posteriores se desplazan y la ejecución dura más. Las marcas de tiempo de inicio reales se conservan en el registro. No hay reintentos automáticos ni redirecciones seguidas, así que una respuesta 3xx es un fallo HTTP en este ejemplo. Si tu carga de trabajo requiere redirecciones, define qué destinos y qué contenido final son aceptables antes de ampliar el harness.

Cada fila del registro contiene el número de intento, la marca de tiempo de inicio en UTC, el modo de ruta, el código de salida de curl, el resultado, los estados HTTP y CONNECT, y cuatro hitos de tiempo acumulativos en segundos. El harness no conserva ni cuerpos de respuesta ni credenciales en sus archivos de resultados. Mantén una nota privada de ejecución aparte con el proveedor, el pool, el objetivo, la versión del cliente y la configuración de sesión.

El denominador de la tasa de éxito son todas las peticiones intentadas, incluidos los tiempos de espera agotados y los fallos de contenido. Los códigos de salida de curl como 28 (tiempo de espera), 7 (fallo de conexión) y 56 (fallo de recepción) describen categorías amplias de fallo, no un componente defectuoso confirmado. Usa la referencia oficial de códigos de salida al investigar.

Las estadísticas de latencia usan solo las respuestas exitosas, y el resumen mantiene los recuentos de fallos junto a ellas. La mediana es el valor central, promediando el par central cuando es necesario. El percentil 95 usa el método del rango más cercano: ordena las muestras exitosas y selecciona el rango ceil(0.95 × sample_count), contando desde uno. Sin éxitos, ambos resúmenes de latencia son null.

Por ejemplo, cuatro éxitos de cinco intentos dan una tasa de éxito del 80 %. Si sus valores de TTFB son 0,10, 0,12, 0,20 y 0,40 segundos, la mediana es 0,16 segundos y el p95 por rango más cercano es 0,40 segundos. Son valores inventados para explicar el cálculo, no resultados medidos de proxies.

Empieza con una comprobación corta y controlada del harness y de tu regla de contenido, y después ejecuta la ventana de observación acordada. Publica la configuración, el tamaño de la muestra, los fallos y la definición de éxito junto a cualquier comparación de latencia. La línea base directa de arriba es ilustrativa; no establece un ranking de proxies.

Fuentes

  1. curl manual: --write-out variables and proxy configuration
  2. everything curl: exit codes
  3. RFC 9110: HTTP Semantics, section 9.3.6 CONNECT
  4. ipify: a simple public IP address API

Etiquetas:ProxiesChoosing proxiesTroubleshooting