Conocimiento

Cómo usar proxies residenciales en C# con HttpClient

Proxies en C#: WebProxy con NetworkCredential, SocketsHttpHandler y PooledConnectionLifetime, IHttpClientFactory, y clientes por identidad para la rotación geo.

Chris Collins

Chris Collins

2 de agosto de 2026 · 8 min de lectura

.NET es un caballo de batalla para la recolección de datos en el backend: I/O asíncrono rápido, tipado fuerte, y un runtime que maneja la alta concurrencia con comodidad. Conectar un proxy residencial a HttpClient son unas pocas líneas, pero C# tiene su propia trampa que no tiene nada que ver con los proxies y todo que ver con cómo se supone que debe usarse HttpClient. Acierta con el ciclo de vida del cliente y la parte del proxy es fácil.

Esta es la entrada de C# de la misma serie que proxies residenciales con Python, con Go y con Node.js: el código que funciona, más los escollos propios de .NET.

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. Cambia el host y las credenciales por otro proveedor; la forma es la misma.

El modelo del gateway en un párrafo

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:

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

country-us apunta a Estados Unidos, sid fija una sesión sticky, ttl mantiene esa IP durante N segundos. Omite sid/ttl y cada conexión nueva rota. La contraseña se mantiene constante. En .NET, ese nombre de usuario va en un NetworkCredential, no en la URL del proxy.

El montaje básico: WebProxy y un handler

Configuras el proxy en un message handler y se lo pasas a HttpClient. Las credenciales van en un WebProxy mediante NetworkCredential, no en línea en el URI.

using System.Net;
var user = Environment.GetEnvironmentVariable("SHIFTER_USER") + "-country-us";
var pass = Environment.GetEnvironmentVariable("SHIFTER_PASS");
var proxy = new WebProxy("http://p.shifter.io:443")
{
Credentials = new NetworkCredential(user, pass) // no user:pass@ en la URL
};
var handler = new SocketsHttpHandler
{
Proxy = proxy,
UseProxy = true,
PooledConnectionLifetime = TimeSpan.FromMinutes(2) // ver la trampa abajo
};
var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(30) };
var ip = await client.GetStringAsync("https://api.ipify.org");
Console.WriteLine(ip); // una IP residencial de EE. UU.

Dos cosas que interiorizar. La cadena user incluye los flags de targeting (-country-us), porque ahí es donde vive la geo. Y las credenciales van en el WebProxy como un NetworkCredential, HttpClient no leerá user:pass@host de un URI de proxy como podría hacer un navegador.

Prefiere SocketsHttpHandler (el handler por defecto en .NET moderno) sobre el más antiguo HttpClientHandler. Es la implementación moderna y multiplataforma y expone PooledConnectionLifetime, que necesitas por la razón de abajo.

Trampa 1: el problema del ciclo de vida de HttpClient

Esta es la trampa específica de .NET, y muerde en dos direcciones opuestas.

Crea un new HttpClient() por cada petición y filtras sockets: cada cliente tiene su propio pool de conexiones, y los clientes desechados dejan sockets atascados en TIME_WAIT. Bajo carga esto agota los puertos disponibles y tu app empieza a lanzar SocketException. El consejo bien conocido es por tanto reutilizar un único HttpClient.

Pero un único cliente mantenido para siempre tiene el problema opuesto: cachea las resoluciones DNS durante la vida del pool de conexiones y nunca nota cuándo cambia la IP de un endpoint. El arreglo no es elegir entre los dos, es PooledConnectionLifetime en SocketsHttpHandler, que recicla las conexiones del pool según un horario para que mantengas la reutilización de conexión sin fijar DNS obsoleto. Ponlo a un par de minutos y reutiliza el cliente.

Reutilizar el cliente también mantiene las conexiones calientes a través del proxy, que es exactamente la sobrecarga que la guía de latencia existe para eliminar.

La forma ASP.NET Core: IHttpClientFactory

Si estás en una app basada en DI, no gestiones HttpClient a mano en absoluto. IHttpClientFactory maneja el ciclo de vida del handler y el reciclaje de conexiones por ti, y te deja configurar el proxy una vez en un cliente con nombre.

services.AddHttpClient("shifter")
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
Proxy = new WebProxy("http://p.shifter.io:443")
{
Credentials = new NetworkCredential(
Environment.GetEnvironmentVariable("SHIFTER_USER") + "-country-us",
Environment.GetEnvironmentVariable("SHIFTER_PASS"))
},
UseProxy = true
});
// luego inyecta IHttpClientFactory y llama a factory.CreateClient("shifter")

La factory rota los handlers subyacentes según su propio horario, así que obtienes pooling y DNS fresco sin tocar PooledConnectionLifetime tú mismo. Este es el camino recomendado para cualquier servicio de larga duración.

Trampa 2: el proxy está en el handler, así que rota con un cliente por identidad

Como el proxy y sus credenciales están horneados en el handler, no puedes variar el nombre de usuario del proxy por petición en un solo HttpClient. Una identidad distinta significa un handler distinto, y por tanto un cliente distinto. El patrón eficiente es cachear un cliente por identidad para que cada sesión mantenga su propio pool de conexiones, la misma idea que un mapa de agent-por-identidad en otros stacks.

using System.Collections.Concurrent;
static readonly ConcurrentDictionary<string, HttpClient> Clients = new();
static HttpClient ClientFor(string country, string sid)
{
var key = $"{country}:{sid ?? "rotate"}";
return Clients.GetOrAdd(key, _ =>
{
var u = Environment.GetEnvironmentVariable("SHIFTER_USER") + "-country-" + country
+ (sid != null ? $"-sid-{sid}-ttl-600" : "");
var handler = new SocketsHttpHandler
{
Proxy = new WebProxy("http://p.shifter.io:443")
{
Credentials = new NetworkCredential(u, Environment.GetEnvironmentVariable("SHIFTER_PASS"))
},
UseProxy = true,
PooledConnectionLifetime = TimeSpan.FromMinutes(2)
};
return new HttpClient(handler);
});
}

Dale a cada unidad lógica de trabajo su propio sid y rota entre unidades, no a mitad de flujo (sticky vs rotativo cubre la distinción), y mapea el trabajo a identidades como describe el post sobre reparto de carga. Cachea y reutiliza estos clientes, no construyas uno por petición.

Trampa 3: acota tu concurrencia

.NET hace trivial disparar miles de peticiones a la vez con Task.WhenAll, y nada te impide abrir cada conexión simultáneamente, lo que agota sockets en tu lado y parece un ataque para el destino. Acota las peticiones en vuelo con un SemaphoreSlim:

var gate = new SemaphoreSlim(8); // como mucho 8 peticiones concurrentes
async Task<string> Fetch(HttpClient client, string url)
{
await gate.WaitAsync();
try { return await client.GetStringAsync(url); }
finally { gate.Release(); }
}

Limita la concurrencia por host de destino, no solo de forma global, para que un sitio frágil no reciba una paliza mientras uno permisivo pasa hambre. Más paralelismo más allá de la tolerancia de un destino compra bloqueos, no throughput (cómo evitar que te bloqueen).

Verifica que de verdad estás en el proxy

Antes de hacer benchmark o depurar cualquier otra cosa, confirma la IP de salida:

var json = await client.GetStringAsync("http://ip-api.com/json");
Console.WriteLine(json); // espera una IP residencial en el país objetivo

Tu propia IP significa que el handler no se está aplicando (normalmente UseProxy dejado en false, o las credenciales en el objeto equivocado). Un cuelgue significa que la salida local está bloqueada. Ambos se cubren en la guía de diagnóstico de timeouts.

Preguntas frecuentes

¿Por qué no funcionan mis credenciales de proxy cuando las pongo en la URL? HttpClient no lee user:pass@host de un URI de proxy. Pon las credenciales en el objeto WebProxy como un NetworkCredential. Como el gateway codifica el targeting en el nombre de usuario, ese nombre de usuario completo (con los flags -country-...) es el nombre de usuario del NetworkCredential, y la contraseña es constante.

¿HttpClientHandler o SocketsHttpHandler? Prefiere SocketsHttpHandler en .NET moderno. Es el handler gestionado por defecto, es multiplataforma, y expone PooledConnectionLifetime, que necesitas para evitar DNS obsoleto en un cliente de larga vida. HttpClientHandler sigue funcionando y delega en él por debajo.

Mi scraper de .NET lanza SocketException bajo carga. ¿Por qué? Casi seguro estás creando un new HttpClient() por petición y agotando sockets. Reutiliza un único cliente (o usa IHttpClientFactory), y pon PooledConnectionLifetime para que la reutilización no fije DNS obsoleto.

¿Cómo roto IPs por petición en C#? Varía el nombre de usuario del proxy, lo que significa un handler distinto y por tanto un cliente distinto. Cachea un HttpClient por identidad en un ConcurrentDictionary para que cada sesión mantenga su propio pool, y elige el cliente por unidad de trabajo. Omite el sid en el nombre de usuario para rotar en cada conexión nueva.

¿Funciona IHttpClientFactory con proxies? Sí. Configura el proxy en el handler primario vía ConfigurePrimaryHttpMessageHandler. La factory gestiona el ciclo de vida del handler y la frescura del DNS por ti, así que es la opción más limpia en cualquier servicio basado en DI.

En resumen

C# más proxies residenciales es rápido en cuanto respetas dos cosas: pon las credenciales en un WebProxy como un NetworkCredential en lugar de en la URL, y acierta con el ciclo de vida de HttpClient. Usa SocketsHttpHandler con PooledConnectionLifetime, o IHttpClientFactory en una app con DI, para que reutilices conexiones sin fijar DNS obsoleto y sin filtrar sockets. Rota geo y sesiones cacheando un cliente por identidad, y acota la concurrencia con un SemaphoreSlim.

Hazlo bien y .NET maneja la recolección concurrente tan bien como cualquier cosa. Apúntalo al gateway residencial, y recuerda que la calidad del pool decide con qué frecuencia reintentas siquiera (reputación de IP). La página de precios tiene los planes por GB para probarlo contra tus propios destinos.

¿Listo para empezar?

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

Comenzar