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: · · 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 con using. HttpClient mantiene 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 un SocketException2.
  • Pero tampoco basta con dejarlo como static sin más. HttpClient solo 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 con SocketsHttpHandler.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.

  1. 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).
  2. 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.
  3. 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.
  4. 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.

Crear con new y destruir con using en cada solicitudEl handler se recrea por completoel pool de conexiones queda vacío cada vezSe abre una conexión TCP cada vezal cerrarla, el puerto queda en TIME-WAITSe siguen consumiendo puertos = agotamiento de socketsReutilizar un único HttpClient staticEl handler mantiene el pool de conexionesObtener un cliente de la fábrica en cada solicitud(lo que se reutiliza no es el cliente, sino el handler, capítulo 4)Se reutilizan las conexiones ya establecidasNo 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.

Crear con new y destruir con using en cada solicitudConvertirlo en static y mantenerlo sin másCompartir el pool de conexiones y dar a las conexiones una vida útilCómo gestionar HttpClient y el pool de conexionesEl pool de conexiones se descarta cada vez→ se siguen consumiendo puertos efímeros= agotamiento de sockets (capítulo 2)Las conexiones permanecen en el pool→ el DNS solo se resuelve al crear la conexión= sigue conectando a la IP antigua tras el cambio (capítulo 3)Recrear las conexiones a intervalos regulares= resuelve tanto el agotamiento como el cambio de DNSLos medios según el entorno son los de la tabla de decisión del capítulo 1- .NET 5+: static + PooledConnectionLifetime- DI / Generic Host: IHttpClientFactory(el cliente se obtiene cada vez; lo compartido es el handler, capítulo 4)- .NET Framework: IHttpClientFactory,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.

Vence a los 2 minutos por defecto. Deja de emitir clientes nuevos,y se destruye en cuanto terminan los procesos en cursoCreateClient (1.a vez)(cliente de usar y tirar)CreateClient (2.a vez)(cliente de usar y tirar)CreateClient tras el recambio(cliente de usar y tirar)Handler 1aquí reside el pool de conexionesPool de conexionesreutiliza las conexiones TCP, no descarta socketsHandler 2se crea de nuevoNuevo pool de conexionesaquí se vuelve a resolver el DNS

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 CreateClient devuelve un HttpClient nuevo, 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 HttpClient producido 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 CookieContainer se 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 por HttpClient.Timeout, la excepción interna contiene un TimeoutException10. Sin embargo, un tiempo de espera provocado por un CancellationTokenSource propio 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 captura HttpRequestException, se le escapará el tiempo de espera.
  • Timeout es un límite para “toda la solicitud”. Si quiere limitar únicamente el establecimiento de la conexión a un tiempo corto, combínelo con SocketsHttpHandler.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 HttpClient carga por defecto toda la respuesta en memoria, para descargas de decenas de MB o más, especifique HttpCompletionOption.ResponseHeadersRead y 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, HttpClient usa la configuración de proxy del sistema operativo, incluida la detección automática. Si sabe que no necesita proxy, desactivarla con HttpClientHandler.UseProxy = false elimina la espera de la detección2. Por el contrario, en un entorno donde el proxy corporativo es obligatorio, especificarlo explícitamente con WebProxy evita 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 con MaxConnectionsPerServer2. En .NET Framework ocurre lo contrario: el valor predeterminado de ServicePointManager.DefaultConnectionLimit es 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

Á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

  1. Microsoft Learn, Guidelines for using HttpClient. Sobre el uso de un cliente de larga duración con PooledConnectionLifetime configurado o un cliente de corta duración producido por IHttpClientFactory en .NET Core/.NET 5+; sobre la recomendación de usar IHttpClientFactory en .NET Framework; y sobre por qué las aplicaciones que usan cookies deben evitar IHttpClientFactory debido a la compartición de CookieContainer 2 3 4 5 6 7 8 9 10

  2. 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 con UseProxy 2 3 4 5 6 7 8 9

  3. Microsoft Learn, IHttpClientFactory with .NET. Sobre que la vida predeterminada del handler es de 2 minutos; sobre que se da por hecho que el HttpClient de la fábrica se usa de forma efímera; sobre que el Dispose del 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

  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., mediante DisableForUnsafeHttpMethods 2 3 4

  5. 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

  6. 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

  7. 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 -anobq o Get-NetTCPConnection los puertos en estado BOUND.  2 3

  8. 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

  9. 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/ServicePoint no se recomiendan para desarrollo nuevo; y sobre que, desde .NET 9, esta propiedad se asigna a PooledConnectionLifetime pero no tiene un efecto real.  2 3

  10. 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 TaskCanceledException con un TimeoutException como excepción interna.  2

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

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.

Volver al blog