No envuelvas HttpClient en un using ── Comunicación HTTP en aplicaciones de negocio C# (patrones de creación, tiempos de espera y reintentos)
· Actualizado el: · Go Komura · CSharp, .NET, HttpClient, IHttpClientFactory, Redes, async/await, Desarrollo Windows, Consultoría técnica
Historial de revisiones (1 actualizaciones, última el 22 Aug 2026)
Registro de los cambios realizados en este artículo. Cuando se archivó una versión previa, sigue siendo legible mediante un enlace permanente con DOI.
- Se ha sustituido la traducción, que estaba abreviada, por una traducción completa del artículo japonés en su versión actual: el texto crece un 55 %. Se han incorporado 12 filas de tabla, 4 notas al pie, 5 bloques de código y 3 diagramas que no estaban en la edición anterior. El contenido no cambia respecto al original japonés; esta edición simplemente ya no lo resume. Además, los enlaces a otros artículos que ya tienen edición en español apuntan ahora a esa edición en lugar de a la japonesa. Leer la versión anterior a esta actualización (DOI: 10.5281/zenodo.21638340)
- Primera publicación
Citar este artículo(DOI: 10.5281/zenodo.21638339)
Este artículo está archivado en Zenodo. A continuación se muestran tanto el DOI que siempre resuelve a la última versión como el DOI fijado a la versión que está leyendo.
Go Komura (2026). No envuelvas HttpClient en un using ── Comunicación HTTP en aplicaciones de negocio C# (patrones de creación, tiempos de espera y reintentos). KomuraSoft LLC. https://doi.org/10.5281/zenodo.21638339 https://comcomponent.com/es/blog/csharp-httpclient-practical-guide/
- DOI (última versión)
- 10.5281/zenodo.21638339
- DOI (esta versión)
- 10.5281/zenodo.22053460
«Al pasar el mediodía, las conexiones a la API externa empiezan a fallar con SocketException» o «cambiamos de servidor de destino, pero la aplicación sigue conectando al servidor antiguo» — el HttpClient de C# es sencillo de usar si solo se trata de llamar a GetAsync, pero si se equivoca en cómo crear y mantener la instancia, puede acabar introduciendo este tipo de fallo que “funciona en el momento, pero se rompe una vez en producción”.
Este artículo, pensado para el caso en el que una aplicación de negocio Windows llama a una API Web externa o a un servicio interno, organiza el patrón de creación correcto de HttpClient, el diseño de tiempos de espera, los reintentos, el manejo de errores y hasta las trampas propias del entorno Windows, en el orden en que suelen surgir dudas en la práctica.
1. Conclusión primero (tabla de decisión)
La forma correcta de mantener HttpClient depende de la configuración de la aplicación. Empecemos con una tabla de decisión.
Antes de nada, aclaremos las siglas que aparecen en la tabla. DI (Dependency Injection: inyección de dependencias) es una técnica de diseño en la que una clase recibe desde fuera las piezas que necesita, en lugar de crearlas ella misma con new; en .NET, Microsoft.Extensions.DependencyInjection es el contenedor que lo implementa. Generic Host es el host genérico de .NET que, además de ese contenedor de DI, se ocupa de la configuración, el registro de logs y el arranque/parada de los procesos en segundo plano; lo tratamos en «Qué es Generic Host» y en «Usar Generic Host + BackgroundService en una aplicación de escritorio». IHttpClientFactory es el proveedor de HttpClient que se apoya en esa DI (capítulo 4). Para que también pueda leerse sin usar DI, explicamos primero el método sin DI (static + PooledConnectionLifetime).
| Tipo de aplicación | Patrón recomendado | Motivo |
|---|---|---|
| Herramienta de consola (.NET) que termina en segundos o minutos | Un único HttpClient static/singleton |
En un proceso de vida corta, el problema del cambio de DNS es prácticamente irrelevante |
| Aplicación de larga duración o servicio de Windows (.NET, sin DI) | HttpClient static + configuración de SocketsHttpHandler.PooledConnectionLifetime |
Resuelve tanto el agotamiento de sockets como el problema del cambio de DNS |
| Aplicación con Generic Host / DI (.NET) | IHttpClientFactory (AddHttpClient) |
Se delega en la fábrica el pool de handlers y su recambio. Con clientes con nombre o tipados se puede separar la configuración por cada destino |
| Aplicación en .NET Framework | Introducir IHttpClientFactory mediante el paquete Microsoft.Extensions.Http |
En .NET Framework es fácil que la creación manual agote los puertos, y el uso de la fábrica también es la recomendación oficial1 |
| La configuración de proxy, cookies o certificados difiere según el destino | Separar un HttpClient por cada configuración (no reutilizarlo) |
La configuración de conexión del handler no puede modificarse tras el envío de la primera solicitud2 |
Dicho esto, adelantamos las conclusiones.
- No cree
new HttpClient()en cada solicitud para destruirlo conusing.HttpClientmantiene internamente un pool de conexiones y está diseñado para reutilizarse. Si se crea y destruye cada vez, se agotan los sockets disponibles bajo carga alta y se produce unSocketException2. - Pero tampoco basta con dejarlo como
staticsin más.HttpClientsolo resuelve el DNS al crear la conexión, así que aunque cambie la dirección IP del destino, sigue usando la conexión antigua. La solución recomendada oficialmente es delimitar la vida de la conexión conSocketsHttpHandler.PooledConnectionLifetime1. - Si usa Generic Host o DI, delegue en
IHttpClientFactory. La fábrica agrupa los handlers en un pool y los recambia cada 2 minutos por defecto, lo que resuelve tanto el agotamiento de sockets como el cambio de DNS3. - El valor de tiempo de espera predeterminado es de 100 segundos. Para una aplicación de negocio, esto se percibe casi como que la aplicación “se haya quedado colgada indefinidamente”, así que hay que definir el requisito para cada destino y configurarlo explícitamente.
- No implemente los reintentos por cuenta propia: empiece por el handler estándar de
Microsoft.Extensions.Http.Resilience. Permite introducir reintentos, disyuntor (circuit breaker) y tiempos de espera con un conjunto de valores predeterminados ya probados, evitando desde el diseño accidentes típicos de los bucles de reintento caseros, como reenviar sin condiciones un POST fallido y provocar un registro duplicado4.
Tenga en cuenta que todos los ejemplos de código de este artículo son fragmentos que extraen solo la parte necesaria para la explicación. logger es un Microsoft.Extensions.Logging.ILogger, ct es el CancellationToken recibido del código que llama, y url y client representan respectivamente la URL de destino y la instancia de HttpClient; todos ellos se dan por preparados en la clase o el método circundante. No funcionarán si se pegan tal cual dentro de Main.
2. Por qué no se debe «crear con using cada vez» — agotamiento de sockets
Como HttpClient implementa IDisposable, un código como el siguiente parece correcto a primera vista.
// Antipatrón: crear y destruir en cada solicitud
public async Task<string> GetDataAsync(string url)
{
using var client = new HttpClient();
return await client.GetStringAsync(url);
}
El problema es que, aunque se llame a Dispose, el sistema operativo no libera el socket de inmediato. Según la especificación TCP, el socket del lado que cierra la conexión permanece un rato en estado TIME_WAIT. Mientras la frecuencia de llamadas sea baja no pasa nada, pero cuando sube la carga se acumulan sockets sin liberar y, en algún momento, deja de poder conectarse de golpe con un SocketException2.
Sin embargo, decir solo que “quedan sockets residuales” no explica del todo por qué se llega al agotamiento. Vale la pena bajar un nivel más. La clave es que el número de puertos propios que se consume en cada conexión es finito.
- Cuando el cliente establece una conexión TCP con el servidor, el puerto de destino (443 en el caso de HTTPS) es fijo, pero el sistema operativo asigna automáticamente el puerto propio a partir de los que estén libres. A esto se le llama puerto efímero (ephemeral port; en la terminología de Windows, puerto dinámico).
- El rango de puertos dinámicos predeterminado de Windows es 49152–65535, es decir, 16384 puertos5. Es el conjunto del que el sistema operativo toma los puertos al establecer conexiones salientes, y se configura por cada transporte (TCP/UDP) y por cada familia (IPv4/IPv6)5. No es un límite de “16384 conexiones simultáneas por máquina”. Como TCP identifica una conexión mediante la cuádrupla “dirección y puerto propios” + “dirección y puerto del otro extremo”6, es perfectamente posible, según la especificación, que el mismo número de puerto forme parte de varias conexiones con destinos distintos. Lo que realmente importa no es tanto el total, sino con qué rapidez un solo proceso va consumiendo ese conjunto.
- Aunque se cierre la conexión, el puerto del lado que cerró primero no puede reutilizarse de inmediato. Para evitar que un paquete antiguo que llegue con retraso se cuele en una conexión nueva que use la misma combinación de puertos, TCP mantiene el estado TIME-WAIT durante un tiempo determinado (2×MSL: el doble del Maximum Segment Lifetime)6.
- Como resultado, un diseño que “abre y cierra una conexión en cada solicitud” termina reteniendo un puerto como usado durante decenas de segundos o varios minutos por cada solicitud. Con 100 solicitudes por segundo, el cálculo es que se consumen 100 de los 16384 huecos por segundo, y el conjunto se agota en pocos minutos.
Quien mantiene el pool de conexiones no es HttpClient en sí, sino el handler que lleva dentro. Recrear la instancia significa recrear también el pool entero, y ahí empieza el agotamiento.
flowchart TB
B1["Crear con new y destruir con using en cada solicitud"] --> B2["El handler se recrea por completo<br/>el pool de conexiones queda vacío cada vez"]
B2 --> B3["Se abre una conexión TCP cada vez<br/>al cerrarla, el puerto queda en TIME-WAIT"] --> B4["Se siguen consumiendo puertos = agotamiento de sockets"]
G1["Reutilizar un único HttpClient static"] --> G2["El handler mantiene el pool de conexiones"]
G1b["Obtener un cliente de la fábrica en cada solicitud<br/>(lo que se reutiliza no es el cliente, sino el handler, capítulo 4)"] --> G2
G2 --> G3["Se reutilizan las conexiones ya establecidas"] --> G4["No se consumen puertos nuevos"]
Figura 1: lo que se reutiliza es el pool de conexiones que mantiene el handler. Al recrear HttpClient se descarta también el pool
Cuando se agota el pool, la siguiente solicitud de conexión falla porque no hay puerto que asignar. El síntoma, aparentemente inexplicable, de que hay margen de CPU y memoria pero simplemente no se puede conectar, se debe a que lo que se agota es otro recurso: el número de puerto. Ahora bien, que aparezca un SocketException no equivale a agotamiento. La caída del otro extremo, un corte de ruta, un fallo de DNS o de TLS suben con el mismo tipo de excepción. La forma de distinguirlos se explica en el apartado siguiente. Las directrices oficiales de Microsoft también indican explícitamente que, aunque se cierre la conexión, el puerto TCP no se libera de inmediato, y que una frecuencia de solicitudes alta puede llegar al límite de puertos disponibles del sistema operativo1.
Lo problemático de este fallo es que prácticamente no se reproduce durante el desarrollo o las pruebas. Con unas pocas operaciones manuales por segundo no se consume el conjunto de puertos, así que se manifiesta solo en las horas punta de producción o solo durante el proceso por lotes de fin de mes. Para investigarlo, cuando ocurra el síntoma, compruebe con los siguientes comandos el número de sockets en TIME_WAIT y el rango de puertos de su entorno.
# Contar el número de sockets en estado TIME_WAIT
(netstat -ano | Select-String "TIME_WAIT").Count
# Comprobar el rango de puertos dinámicos del propio entorno (por defecto: inicio 49152, 16384 puertos)
netsh int ipv4 show dynamicport tcp
Aquí no debe comparar el total de TIME_WAIT de toda la máquina con el 16384 del rango. Si esa máquina se conecta a muchos destinos, el total será grande, pero eso solo está contando conexiones sanas hacia otros destinos y no significa que esta aplicación esté cerca del agotamiento. El propio procedimiento de diagnóstico de Microsoft indica explícitamente que el mero hecho de tener muchos TIME_WAIT no es prueba de agotamiento (solo indica que podría agotarse en el futuro)7.
Lo que hay que observar es el desglose por proceso y por destino, y cómo evoluciona con el tiempo.
# Agrupar por proceso y estado. Si el TIME_WAIT se concentra en un PID, ese proceso es el responsable
Get-NetTCPConnection | Group-Object State, OwningProcess |
Sort-Object Count -Descending | Select-Object -First 10 Count, Name
# Agrupar solo los destinos sospechosos por dirección local y extremo remoto
Get-NetTCPConnection -State TimeWait |
Group-Object LocalAddress, RemoteAddress, RemotePort |
Sort-Object Count -Descending | Select-Object -First 10 Count, Name
Dicho esto, si realmente hay agotamiento se confirma por los síntomas. Microsoft indica tres puntos de verificación7.
| Qué comprobar | Cómo verlo |
|---|---|
| Si fallan en bloque las conexiones salientes | Pruebe el acceso a un recurso compartido, un RDP a otro servidor, un telnet. Si falla solo un destino, no es agotamiento |
| El registro de eventos | ID de evento 4227 / 4231 en el registro del sistema |
| Concentración en un PID concreto | Con netstat -anob, si el TIME_WAIT se concentra en un único PID |
La evolución en el tiempo es una prueba más fiable que el número absoluto. Si, justo después de reiniciar la aplicación, el número sube de forma constante y no baja durante varios minutos aunque se detenga la carga, se puede dar casi por seguro que el patrón de creación es la causa. Si, por el contrario, sube y baja según la carga y llega a un techo, las conexiones se están reutilizando. Tenga en cuenta que los sockets en TIME_WAIT están desligados del proceso, por lo que a veces no se puede identificar la aplicación llamante por la columna de PID; en ese caso, filtre por la columna de dirección externa (destino). En Windows 10 / Windows Server 2016 en adelante, también se pueden ver con netstat -anobq o Get-NetTCPConnection los puertos que han salido de TIME_WAIT y pasado a BOUND7.
Cabe señalar que el HttpClient recibido a través de IHttpClientFactory queda fuera de este problema. En un cliente producido por la fábrica, aunque se llame a Dispose, el handler (donde reside el pool de conexiones) no se destruye, así que envolverlo en un using es seguro3.
3. Convertirlo en static no es la solución completa — el problema del cambio de DNS
Convertir HttpClient en static como medida contra el agotamiento de sockets va en la dirección correcta, pero por sí solo deja otro problema sin resolver. HttpClient solo resuelve el DNS al crear la conexión, y tampoco consulta el TTL del registro DNS2. Mientras la conexión siga viva dentro del pool, seguirá conectando a la IP antigua aunque cambie la dirección IP del destino.
El incidente de «cambiamos el DNS con el failover, pero la aplicación siguió apuntando al servidor antiguo hasta que se reinició» tiene este mecanismo como causa. La solución que recomiendan las directrices oficiales es delimitar la vida de la conexión con SocketsHttpHandler.PooledConnectionLifetime1.
// Patrón recomendado en .NET (Core) / .NET 5+:
// hacer que la conexión se recree periódicamente para seguir los cambios de DNS
private static readonly HttpClient SharedClient = new(new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(2)
});
La conexión que llega al final de su vida útil se recrea en la siguiente solicitud, y en ese momento se vuelve a resolver el DNS. El valor se decide según con qué rapidez se quiera seguir los cambios de DNS. El ejemplo de la documentación oficial usa 2 minutos, pero para un sistema interno cuyo destino apenas cambia, un valor más largo tampoco supone ningún problema1.
Tenga en cuenta que SocketsHttpHandler es una implementación de .NET Core 2.1 en adelante, y no está disponible en .NET Framework. En .NET Framework, use el IHttpClientFactory del siguiente capítulo1.
Aquí resumimos en un solo lugar los entornos soportados. Consulte esta tabla para ver cuáles de los recursos mencionados en este artículo están disponibles en su entorno.
| Recurso | .NET Framework 4.8 | .NET Core 2.1+ / .NET 5+ |
|---|---|---|
HttpClient en sí |
Disponible (implementación interna: HttpWebRequest / ServicePoint) |
Disponible (implementación interna: SocketsHttpHandler) |
SocketsHttpHandler.PooledConnectionLifetime |
No disponible | Disponible. Primera opción para seguir los cambios de DNS1 |
IHttpClientFactory |
Disponible con el paquete Microsoft.Extensions.Http. Recomendación oficial1 |
Disponible |
ServicePointManager.DefaultConnectionLimit |
Disponible (atención: el valor predeterminado es apenas 2)8 | No afecta a HttpClient. El límite se especifica con MaxConnectionsPerServer |
ServicePoint.ConnectionLeaseTimeout |
Disponible (ver más abajo) | Prácticamente sin efecto real9 |
En .NET Framework, la primera opción, tal como indican las directrices oficiales, es introducir IHttpClientFactory mediante el paquete Microsoft.Extensions.Http1. Aun así, seguramente hay muchos casos en los que “no hay margen para empezar introduciendo DI en una aplicación WinForms grande ya existente”. La solución intermedia para esos casos es compartir un único HttpClient y, al mismo tiempo, delimitar la vida de la conexión con ServicePoint.ConnectionLeaseTimeout.
using System;
using System.Net;
using System.Net.Http;
// Solución intermedia en .NET Framework 4.8:
// compartir el HttpClient y delimitar la vida de la conexión desde ServicePoint para seguir los cambios de DNS
private static readonly Uri ApiBase = new Uri("https://order.example.co.jp/");
private static readonly HttpClient SharedClient = CreateSharedClient();
private static HttpClient CreateSharedClient()
{
// Debe ejecutarse siempre antes de la primera solicitud. Si se olvida llamar aquí,
// ConnectionLeaseTimeout se queda en su valor predeterminado -1 (mantiene la conexión indefinidamente)
// y no se sigue el cambio de DNS = la medida de este apartado no tiene ningún efecto
ServicePoint sp = ServicePointManager.FindServicePoint(ApiBase);
sp.ConnectionLeaseTimeout = (int)TimeSpan.FromMinutes(2).TotalMilliseconds; // la unidad son milisegundos
return new HttpClient { BaseAddress = ApiBase };
}
ConnectionLeaseTimeout es una configuración que “cierra la conexión en cuanto termina de procesar una solicitud, una vez transcurrido el tiempo indicado”, pensada para situaciones en las que se quiere volver a establecer la conexión periódicamente, como al cambiar de balanceador de carga9. La conexión cerrada se recrea en la siguiente solicitud, momento en el que se vuelve a resolver el DNS. La idea es exactamente la misma que PooledConnectionLifetime; considérelo el equivalente en el lado de .NET Framework.
Sin embargo, se indica oficialmente que las API de la familia WebRequest, incluido ServicePoint, no deben usarse en desarrollo nuevo9. Esto es únicamente un remedio para el caso de seguir prolongando la vida de .NET Framework; al migrar a .NET, sustitúyalo por PooledConnectionLifetime o por IHttpClientFactory. Los puntos a tener en cuenta al migrar se resumen en «Lista de verificación antes de migrar de .NET Framework a .NET».
Los dos fallos vistos hasta aquí provienen, en ambos casos, de no haber decidido “cuándo recrear la conexión”. La relación entre los capítulos 2 y 3 se resume en el siguiente diagrama.
flowchart TB
Q["Cómo gestionar HttpClient y el pool de conexiones"]
Q -->|"Crear con new y destruir con using en cada solicitud"| A["El pool de conexiones se descarta cada vez<br/>→ se siguen consumiendo puertos efímeros<br/>= agotamiento de sockets (capítulo 2)"]
Q -->|"Convertirlo en static y mantenerlo sin más"| B["Las conexiones permanecen en el pool<br/>→ el DNS solo se resuelve al crear la conexión<br/>= sigue conectando a la IP antigua tras el cambio (capítulo 3)"]
Q -->|"Compartir el pool de conexiones y dar a las conexiones una vida útil"| C["Recrear las conexiones a intervalos regulares<br/>= resuelve tanto el agotamiento como el cambio de DNS"]
C --> C1["Los medios según el entorno son los de la tabla de decisión del capítulo 1<br/>- .NET 5+: static + PooledConnectionLifetime<br/>- DI / Generic Host: IHttpClientFactory<br/> (el cliente se obtiene cada vez; lo compartido es el handler, capítulo 4)<br/>- .NET Framework: IHttpClientFactory,<br/> o ConnectionLeaseTimeout si resulta complicado"]
Figura 2: tanto “no reutilizar nunca” como “reutilizar sin límite” son errores. La solución correcta es reutilizar el pool de conexiones dándoles, además, una vida útil. Lo que se comparte varía según el método (en la fábrica no es el cliente, sino el handler)
4. Si usa DI, IHttpClientFactory
En aplicaciones que usan Generic Host o un contenedor de DI, IHttpClientFactory (AddHttpClient) es la primera opción. Para una explicación del propio Generic Host, consulte «Qué es Generic Host», y para su introducción en una aplicación de escritorio, «Usar Generic Host + BackgroundService en una aplicación de escritorio».
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// Cliente con nombre: separa la configuración por cada destino
builder.Services.AddHttpClient("OrderApi", client =>
{
client.BaseAddress = new Uri("https://order.example.co.jp/");
client.Timeout = TimeSpan.FromSeconds(10);
});
La forma más rápida de ver qué hace la fábrica es con un diagrama. Todo este mecanismo se resume en que HttpClient y el handler que mantiene el pool de conexiones son cosas distintas.
flowchart TB
C1["CreateClient (1.a vez)<br/>(cliente de usar y tirar)"]
C2["CreateClient (2.a vez)<br/>(cliente de usar y tirar)"]
C3["CreateClient tras el recambio<br/>(cliente de usar y tirar)"]
H1["Handler 1<br/>aquí reside el pool de conexiones"]
P1["Pool de conexiones<br/>reutiliza las conexiones TCP, no descarta sockets"]
H2["Handler 2<br/>se crea de nuevo"]
P2["Nuevo pool de conexiones<br/>aquí se vuelve a resolver el DNS"]
C1 --> H1
C2 --> H1
H1 --> P1
H1 -->|"Vence a los 2 minutos por defecto. Deja de emitir clientes nuevos,<br/>y se destruye en cuanto terminan los procesos en curso"| H2
C3 --> H2
H2 --> P2
Figura 3: el pool de handlers de IHttpClientFactory y su recambio periódico
De este diagrama se desprenden las dos conclusiones planteadas al principio del artículo. “Usar y tirar, mal” porque, con un new HttpClient() casero, la capa superior (cliente) y la inferior (handler / pool de conexiones) mueren juntas en una relación uno a uno, y cada vez que se descarta el cliente se descartan también los sockets. “Static a medias” porque, al fijarlo como static, la capa inferior queda fija y nunca se destruye — se protegen los sockets, pero la flecha que representa el recambio del handler (= la nueva resolución de DNS) nunca llega a producirse. La fábrica satisface ambas cosas a la vez: mantiene la capa superior como algo de usar y tirar, comparte solo la capa inferior y, además, la recambia periódicamente.
Hay tres puntos del funcionamiento de la fábrica que conviene retener.
- Los handlers se agrupan en un pool y se recambian cada 2 minutos por defecto. Cada llamada a
CreateClientdevuelve unHttpClientnuevo, pero el handler subyacente (el pool de conexiones) se comparte, por lo que no se produce agotamiento de sockets, y el recambio periódico también permite seguir los cambios de DNS3. - El
HttpClientproducido por la fábrica se da por hecho que se usa de forma efímera. Si guarda la instancia recibida en un campo singleton, deja de participar en el recambio de handlers y ya no sigue los cambios de DNS. Por el mismo motivo, evite también inyectar un cliente tipado en un servicio singleton3. - Hay que tener cuidado en aplicaciones que dependen de cookies. Como resultado de agrupar los handlers, el
CookieContainerse comparte de forma involuntaria. Si usa cookies, la documentación oficial recomienda evitar la fábrica, o bien desactivar el manejo de cookies y añadir las cabeceras manualmente1.
Todo lo relacionado con la obtención de tokens al llamar a una API con autenticación (por ejemplo, una API protegida con Microsoft Entra ID) se trata en «Integrar la autenticación de Entra ID en una aplicación WinForms/WPF», así que consulte ese artículo.
5. Diseño de tiempos de espera — los 100 segundos predeterminados son demasiado largos para una aplicación de negocio
El valor predeterminado de HttpClient.Timeout es de 100 segundos10. En una aplicación de negocio donde la API se llama como extensión de una operación en pantalla, hacer esperar 100 segundos equivale a que la aplicación “se haya quedado colgada”, así que hay que configurarlo explícitamente para cada destino.
// Tiempo de espera predeterminado de todo el cliente
client.Timeout = TimeSpan.FromSeconds(10);
// Para acortar/alargar el tiempo solo en una solicitud concreta, use CancellationTokenSource
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
HttpResponseMessage response = await client.GetAsync(url, cts.Token);
Los puntos a tener en cuenta en el diseño son los siguientes.
- La excepción al agotarse el tiempo de espera es
TaskCanceledException. Desde .NET 5, cuando el tiempo de espera se agota porHttpClient.Timeout, la excepción interna contiene unTimeoutException10. Sin embargo, un tiempo de espera provocado por unCancellationTokenSourcepropio como el del ejemplo anterior no lleva excepción interna. La forma fiable de distinguir un tiempo de espera de una cancelación iniciada por el usuario es comprobar, no la excepción interna, sino si el token pasado por el código llamante ya está cancelado (ejemplo de código en el capítulo 7). Tenga cuidado: si en el bloque catch solo capturaHttpRequestException, se le escapará el tiempo de espera. Timeoutes un límite para “toda la solicitud”. Si quiere limitar únicamente el establecimiento de la conexión a un tiempo corto, combínelo conSocketsHttpHandler.ConnectTimeout. Un requisito como “renunciar en 3 segundos si el servidor está caído, pero esperar 60 segundos si la respuesta es grande en condiciones normales” se puede expresar combinando ambos2.- En la descarga de archivos grandes, evite el búfer predeterminado. Como
HttpClientcarga por defecto toda la respuesta en memoria, para descargas de decenas de MB o más, especifiqueHttpCompletionOption.ResponseHeadersReady procese el contenido como flujo (stream)2.
Para extraer los valores de tiempo de espera a appsettings.json y variarlos según el entorno, puede aplicar directamente la tabla de decisión de «Gestión práctica de la configuración en aplicaciones de negocio Windows».
Tenga en cuenta que, al llamar a la comunicación HTTP desde WinForms/WPF, bloquear con .Result o .Wait() provoca un interbloqueo (deadlock) del hilo de interfaz de usuario. Esta trampa clásica se explica en detalle en «Tabla de decisión práctica de async/await en C#», así que se recomienda leerla antes de escribir el código de comunicación.
6. Reintentos — use el handler de resiliencia estándar en lugar de un bucle propio
Como la red puede fallar de forma pasajera, todo proceso que llame a una API externa necesita reintentos. Sin embargo, un reintento casero con un bucle for y Task.Delay obliga a implementar correctamente por cuenta propia todos los puntos siguientes, y no compensa el esfuerzo.
- Distinguir entre fallos en los que sí conviene reintentar (tiempo de espera, HTTP 408/429/5xx) y fallos en los que reintentar no sirve de nada (HTTP 400/401/404)
- Excluir los métodos HTTP cuya reejecución provoca un accidente (como la ejecución duplicada de un registro mediante POST)
- El retroceso exponencial y el jitter (la variación aleatoria que evita que todos los clientes reenvíen a la vez y vuelvan a tumbar el servidor)
- Un disyuntor (circuit breaker) que detenga las llamadas cuando el fallo persiste
El handler estándar del paquete Microsoft.Extensions.Http.Resilience ofrece todo este conjunto con valores predeterminados de eficacia probada4.
builder.Services.AddHttpClient("OrderApi", client =>
{
client.BaseAddress = new Uri("https://order.example.co.jp/");
})
.AddStandardResilienceHandler(); // Conjunto estándar de reintentos + disyuntor + tiempo de espera
Los valores predeterminados del handler estándar son: 30 segundos de tiempo de espera para toda la solicitud, hasta 3 reintentos con retroceso exponencial (retraso inicial de 2 segundos, con jitter), 10 segundos de tiempo de espera por intento, y trata como errores temporales los códigos HTTP 408/429/5xx y HttpRequestException4.
Hay un punto del valor predeterminado al que hay que prestar atención: por defecto, el handler estándar reintenta todos los métodos HTTP. En las API donde ejecutar dos veces un POST de registro sería un problema, desactive el reintento para los métodos no seguros4.
httpClientBuilder.AddStandardResilienceHandler(options =>
{
// Desactivar la reejecución de POST/PUT/DELETE, etc.
options.Retry.DisableForUnsafeHttpMethods();
});
Tenga en cuenta que los reintentos solo resuelven fallos temporales. Síntomas como que la conexión esté establecida pero no fluyan datos, o que la respuesta sea extremadamente lenta, suelen ser problemas de la capa TCP, y para diagnosticarlos puede resultar útil el método tratado en «Causa y diagnóstico de la interrupción de la comunicación de una cámara industrial por retransmisión TCP».
7. Manejo de errores — cómo tratar los códigos de estado
HttpClient no lanza una excepción ante fallos “en los que sí llegó una respuesta HTTP”, como un 404 o un 500. Lo que sí provoca una excepción es no haber obtenido respuesta alguna: fallo de conexión, tiempo de espera agotado o cancelación. Conviene escribir el código distinguiendo conscientemente estas dos categorías.
try
{
using HttpResponseMessage response = await client.GetAsync(url, ct);
if (!response.IsSuccessStatusCode)
{
// La respuesta llegó, pero indica un fallo: se puede distinguir por el código de estado
if (response.StatusCode == HttpStatusCode.NotFound)
{
return null; // Ejemplo: tratar "no existe" como un caso normal
}
response.EnsureSuccessStatusCode(); // El resto pasa a HttpRequestException
}
return await response.Content.ReadFromJsonAsync<Order>(ct);
}
catch (HttpRequestException ex)
{
// Fallo de conexión, o fallo de estado provocado por EnsureSuccessStatusCode.
// Desde .NET 5, ex.StatusCode permite consultar el código de estado del fallo
logger.LogError(ex, "Fallo al llamar a la API de pedidos. StatusCode={StatusCode}", ex.StatusCode);
throw;
}
catch (TaskCanceledException) when (ct.IsCancellationRequested)
{
// Cancelación mediante el token recibido del código llamante (por ejemplo, se cerró la pantalla).
// No es un error, así que se propaga tal cual sin ensuciar el log
throw;
}
catch (TaskCanceledException ex)
{
// Vencimiento de HttpClient.Timeout, o del CTS propio usado para el tiempo de espera
logger.LogError(ex, "La API de pedidos superó el tiempo de espera");
throw;
}
Una decisión como “tratar el 404 como excepción o como null” depende de la semántica de la API de destino. Convertir todo en excepción de un plumazo con EnsureSuccessStatusCode hace que el bloque catch del lado llamante crezca sin control. El criterio para trazar la línea entre lo que se convierte en excepción y lo que se expresa mediante el valor de retorno es directamente aplicable desde «Manejo práctico de captura de excepciones, logs y errores».
Para el envío y la recepción de JSON, si usa GetFromJsonAsync / PostAsJsonAsync / ReadFromJsonAsync de System.Net.Http.Json, se evita tener que escribir a mano la serialización pasando por una cadena de texto.
8. Trampas específicas de las aplicaciones de negocio Windows
Para terminar, resumimos las trampas habituales en la práctica en entornos Windows.
- La primera solicitud es lenta por la detección automática del proxy. Por defecto en Windows,
HttpClientusa la configuración de proxy del sistema operativo, incluida la detección automática. Si sabe que no necesita proxy, desactivarla conHttpClientHandler.UseProxy = falseelimina la espera de la detección2. Por el contrario, en un entorno donde el proxy corporativo es obligatorio, especificarlo explícitamente conWebProxyevita el problema de “funciona en la máquina de desarrollo pero no en el servidor”. - Configure el proxy antes de la primera solicitud. Los ajustes de conexión del handler no surten efecto si se modifican después de haber enviado ya una solicitud2.
- El valor predeterminado del número de conexiones simultáneas es opuesto entre .NET y .NET Framework. En .NET (
SocketsHttpHandler), el número de conexiones simultáneas de HTTP/1.1 es ilimitado por defecto, así que un gran volumen de solicitudes concurrentes puede hacer que las conexiones sigan aumentando hasta chocar con los límites del firewall o del servidor. En procesos con alta concurrencia, establezca un límite conMaxConnectionsPerServer2. En .NET Framework ocurre lo contrario: el valor predeterminado deServicePointManager.DefaultConnectionLimites pequeño, 2 (en entornos que no son ASP.NET), lo que provoca el problema opuesto, con solicitudes concurrentes que quedan esperando internamente y acaban agotando el tiempo de espera. Si necesita aumentar la concurrencia en .NET Framework, eleve explícitamente este límite8. - Al llamar desde un servicio de Windows, el contexto de proxy y TLS es distinto al del usuario. La cuenta con la que se ejecuta el servicio no tiene la configuración de proxy ni las credenciales del usuario, lo que es una causa habitual del fallo de comunicación clásico “funciona con el usuario interactivo, pero no en el servicio”. Para el contexto de ejecución propio de los servicios, consulte «Cómo crear y operar servicios de Windows».
- No incruste en el código la URL de destino ni las claves de API. Delegue el cambio de destino en un archivo de configuración (véase «Gestión práctica de la configuración»), y el almacenamiento de información confidencial en el método de «Evitar la configuración en texto plano con DPAPI».
Resumen
En la práctica con HttpClient, la calidad depende más de “cómo se mantiene” que de “cómo se llama”. Crearlo en cada solicitud provoca agotamiento de sockets, y convertirlo ingenuamente en static provoca que no se siga el cambio de DNS; ninguno de los dos se hace visible durante el desarrollo. En .NET, la respuesta es una instancia compartida con PooledConnectionLifetime o IHttpClientFactory; en .NET Framework, introducir IHttpClientFactory. Además de esto, hay que fijar el tiempo de espera de forma explícita para cada destino y delegar los reintentos en el handler de resiliencia estándar — solo llegando hasta aquí se obtiene una aplicación de negocio que da por hecho que “la red falla de vez en cuando”.
Revisar la comunicación de una aplicación existente (fallos que solo ocurren en las horas punta, ordenar el diseño de los tiempos de espera, implementar de nuevo una integración con una API externa) suele requerir decisiones que solo pueden tomarse viendo el código real y el entorno de operación, así que, si tiene dudas, no dude en consultarnos.
Artículos relacionados
- Tabla de decisión práctica de async/await en C#
- Qué es Generic Host
- Usar Generic Host + BackgroundService en una aplicación de escritorio
- No solo appsettings.json — gestión práctica de la configuración en aplicaciones de negocio Windows
- Integrar la autenticación de Entra ID en una aplicación WinForms/WPF
- Causa y diagnóstico de la interrupción de la comunicación de una cámara industrial por retransmisión TCP
- El malentendido de que se puede recibir (Receive) por cada unidad enviada (Send) en TCP
- Manejo práctico de captura de excepciones, logs y errores
- Almacenamiento de información confidencial en aplicaciones Windows - evitar la configuración en texto plano con DPAPI
Áreas de consultoría relacionadas
KomuraSoft LLC se encarga del desarrollo de aplicaciones de negocio Windows que incluyen integración con APIs externas, así como de la consultoría técnica para investigar fallos de comunicación en aplicaciones existentes (agotamiento de sockets, tiempos de espera, fallos de conexión intermitentes) y definir la política de corrección.
Referencias
-
Microsoft Learn, Guidelines for using HttpClient. Sobre el uso de un cliente de larga duración con
PooledConnectionLifetimeconfigurado o un cliente de corta duración producido porIHttpClientFactoryen .NET Core/.NET 5+; sobre la recomendación de usarIHttpClientFactoryen .NET Framework; y sobre por qué las aplicaciones que usan cookies deben evitarIHttpClientFactorydebido a la compartición deCookieContainer. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 -
Microsoft Learn, HttpClient Class. Sobre cómo la creación en cada solicitud provoca agotamiento de sockets y
SocketException; sobre que el DNS solo se resuelve al crear la conexión y no se consulta el TTL; sobre que la configuración de conexión del handler no se puede cambiar después de la primera solicitud; sobre que el número de conexiones simultáneas de HTTP/1.1 es ilimitado por defecto; sobre la recomendación de streaming para descargas grandes; y sobre el comportamiento predeterminado del proxy y su desactivación conUseProxy. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 -
Microsoft Learn, IHttpClientFactory with .NET. Sobre que la vida predeterminada del handler es de 2 minutos; sobre que se da por hecho que el
HttpClientde la fábrica se usa de forma efímera; sobre que elDisposedel cliente de la fábrica no destruye el handler; y sobre que inyectar un cliente tipado en un singleton hace que deje de seguir los cambios de DNS. ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, Build resilient HTTP apps: Key development patterns. Sobre las 5 estrategias que configura
AddStandardResilienceHandler(limitador de tasa / tiempo de espera total de 30 segundos / hasta 3 reintentos con retroceso exponencial / disyuntor / tiempo de espera de 10 segundos por intento); sobre los códigos de estado objetivo (408/429/5xx) y las excepciones; y sobre la desactivación del reintento de POST, etc., medianteDisableForUnsafeHttpMethods. ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, The default dynamic port range for TCP/IP has changed in Windows Vista and in Windows Server 2008. Sobre que, desde Windows Vista / Windows Server 2008, el rango de puertos dinámicos predeterminado empieza en 49152 y termina en 65535, y sobre cómo comprobar el rango actual con
netsh int ipv4 show dynamicport tcp. ↩ ↩2 -
IETF, RFC 9293 - Transmission Control Protocol (TCP), Section 3.3.2. Sobre el estado TIME-WAIT de TCP y el mecanismo por el cual la espera de 2×MSL (Maximum Segment Lifetime) evita que un paquete retrasado se mezcle con una conexión posterior. También sobre que una conexión TCP se identifica por el par de sockets (dirección y puerto) de ambos extremos. ↩ ↩2
-
Microsoft Learn, TCP/IP port exhaustion troubleshooting. Sobre que el rango de puertos dinámicos se configura por cada transporte; sobre que tener una gran cantidad de TIME_WAIT no es en sí mismo una prueba de agotamiento, sino que solo indica que podría agotarse en el futuro; sobre confirmarlo mediante el fallo en bloque de las conexiones salientes, los ID de evento 4227/4231 y la concentración de TIME_WAIT en un único PID; y sobre que, desde Windows 10 / Windows Server 2016, también se pueden ver con
netstat -anobqoGet-NetTCPConnectionlos puertos en estado BOUND. ↩ ↩2 ↩3 -
Microsoft Learn, ServicePointManager.DefaultConnectionLimit Property. Sobre que el número predeterminado de conexiones simultáneas es 10 en aplicaciones alojadas en ASP.NET y 2 en el resto (aplicaciones de escritorio, etc.). ↩ ↩2
-
Microsoft Learn, ServicePoint.ConnectionLeaseTimeout Property. Sobre que el valor predeterminado es -1 (indefinido) y se especifica en milisegundos; sobre que, transcurrido el tiempo indicado, la conexión se cierra en cuanto termina de procesar una solicitud; sobre que está pensado para situaciones como el balanceo de carga, donde se quiere volver a establecer la conexión periódicamente; sobre que las API de la familia
WebRequest/ServicePointno se recomiendan para desarrollo nuevo; y sobre que, desde .NET 9, esta propiedad se asigna aPooledConnectionLifetimepero no tiene un efecto real. ↩ ↩2 ↩3 -
Microsoft Learn, HttpClient.Timeout Property. Sobre que el valor predeterminado es de 100 segundos, y sobre que, desde .NET 5, al agotarse el tiempo de espera se lanza un
TaskCanceledExceptioncon unTimeoutExceptioncomo excepción interna. ↩ ↩2
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
El CSV no es «solo texto»: guía práctica del manejo de CSV en aplicaciones empresariales con C# (codificación de caracteres, compatibilidad con Excel, protección contra inyección)
Repasamos los incidentes típicos de entrada/salida de CSV en aplicaciones empresariales -el parseo casero con Split(','), la corrupción d...
Proxy interno de la empresa y aplicaciones de Windows — cómo se resuelve el proxy en WinINET, WinHTTP y .NET
El navegador conecta, pero la app empresarial no atraviesa el proxy interno. La causa suele ser una discrepancia sobre qué configuración ...
Cuando su aplicación Windows de desarrollo propio es tratada como virus — cómo abordar los falsos positivos de Microsoft Defender y su impacto en el rendimiento
Procedimiento oficial para resolver falsos positivos de Microsoft Defender en apps Windows propias: por qué ocurren, cómo reportarlos, re...
¿Las aplicaciones empresariales funcionan en Windows para Arm? — La realidad de la emulación x64 (Prism), las DLL nativas y COM
Respondemos si las aplicaciones empresariales funcionan en Windows para Arm: la emulación x64 (Prism), las capas que no funcionan (contro...
Iconos de la bandeja del sistema y notificaciones toast en aplicaciones Windows — los escollos de NotifyIcon y cómo elegir el AppNotification adecuado
Organiza la implementación de la residencia en la bandeja del sistema y las notificaciones toast en aplicaciones Windows empresariales: e...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Hilo de UI y temporizadores
Hilo de UI de WPF / WinForms, flujos asíncronos, Dispatcher y diseño de temporizadores.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Desarrollo de aplicaciones para Windows
Porque el diseño y la implementación de la integración con APIs externas y la comunicación HTTP forman parte del ámbito de consultoría práctica del desarrollo de aplicaciones Windows.
Consultoría técnica y revisión de diseño
Porque revisar los problemas de comunicación de una aplicación existente (agotamiento de sockets, diseño de tiempos de espera) corresponde a una consultoría técnica que implica una revisión de diseño.
Preguntas frecuentes
Preguntas habituales en las consultas sobre el tema del artículo.
- HttpClient implementa IDisposable, ¿pero no se debe envolver en un using?
- El problema es el patrón de "crear y destruir en cada solicitud". HttpClient mantiene internamente un pool de conexiones y está diseñado para reutilizarse durante toda la vida de la aplicación. Si se crea y destruye cada vez, los sockets permanecen un tiempo en estado TIME_WAIT incluso después de liberarse, de modo que con carga alta se agotan los sockets disponibles y se produce un SocketException. Destrúyalo una sola vez al finalizar la aplicación, u obténgalo a través de IHttpClientFactory. El HttpClient obtenido mediante la fábrica no destruye el handler aunque se llame a Dispose, por lo que envolverlo en un using no supone ningún problema.
- ¿Debería migrar desde WebClient o HttpWebRequest de .NET Framework?
- Se recomienda unificar el código nuevo en torno a HttpClient. WebClient y HttpWebRequest son APIs antiguas que se mantienen por compatibilidad, y Microsoft también recomienda usar HttpClient en el desarrollo nuevo. No es necesario reemplazar todo el código existente de golpe, pero ir migrando a HttpClient cada vez que se toque la parte de comunicación facilita el mantenimiento en cuanto a control de tiempos de espera, soporte de async y facilidad de pruebas.
- ¿Cuántas veces y con qué intervalo se deberían hacer los reintentos?
- Es más seguro partir de los valores predeterminados del handler estándar de Microsoft.Extensions.Http.Resilience (máximo 3 intentos, retroceso exponencial con jitter, retraso inicial de 2 segundos) que decidirlo por cuenta propia. Lo importante no es tanto el número de intentos como determinar si la solicitud admite reintentos: para operaciones como un POST, donde reejecutarla puede provocar un registro duplicado, desactive el reintento por defecto o active los reintentos solo después de garantizar la idempotencia en el servidor (la propiedad de que el resultado no cambie aunque la misma solicitud se reciba dos veces).
- ¿Por qué solo la primera solicitud es extremadamente lenta en un entorno con proxy corporativo?
- Con la configuración predeterminada de Windows, HttpClient intenta detectar automáticamente el proxy, por lo que la primera conexión puede tardar debido al proceso de detección. En entornos donde se sabe que no se necesita proxy (por ejemplo, comunicación interna entre servidores), desactivar la detección automática poniendo UseProxy en false en HttpClientHandler mejora el comportamiento. Por el contrario, en entornos corporativos donde el proxy es obligatorio, es más estable especificarlo explícitamente con WebProxy en lugar de depender de la detección automática.
Perfil del autor
Página de presentación del autor del artículo.
Go Komura
Representante de KomuraSoft LLC
Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.