HttpClient nuevo por petición: así se agotan los puertos TCP de tu proceso
Un HttpClient por petición crea un pool de conexiones nuevo cada vez. Con tráfico alto los puertos efímeros se agotan y las llamadas salientes fallan, aunque CPU, memoria y base de datos estén planos.

Hay una familia de fallos en producción que no deja rastro en los dashboards: las llamadas salientes a otro servicio empiezan a dar timeout, aparece SocketException, la latencia sube y las peticiones fallan de forma intermitente. CPU normal. Memoria normal. Base de datos normal. Reiniciar el proceso lo arregla durante un rato, y luego el problema vuelve.
El culpable suele estar debajo del código C#: las conexiones TCP y, sobre todo, cómo se gestiona su ciclo de vida. El caso típico es un using var client = new HttpClient() dentro de cada petición. Con poco tráfico funciona. Cuando el tráfico sube, se rompe.
Qué se agota exactamente
Cada conexión TCP saliente necesita un puerto de origen local, tomado del rango de puertos efímeros del sistema operativo. Ese rango no es infinito. Y cerrar una conexión no devuelve el puerto al instante: tras ciertos cierres, el endpoint se queda en TIME_WAIT un rato. No es un bug, es TCP protegiéndose de que paquetes retrasados de una conexión anterior se confundan con los de una nueva.
El problema aparece cuando el ritmo al que se crean conexiones supera al que el sistema tarda en reciclar esos recursos. Multiplica tasa de creación por tiempo de vida de cada conexión y tendrás la presión sobre el rango de puertos. Pongamos 500 conexiones salientes nuevas por segundo: el número de endpoints implicados se dispara. El método C# puede ser rapidísimo; lo que se queda sin aire es la capa de abajo. Esto no es "mi método va lento", es un problema de ciclo de vida de recursos.
Aquí está el malentendido más común: using libera el objeto, pero el objeto y los recursos de red que cuelgan de él no tienen el mismo ciclo de vida. Y cada HttpClient trae su propio pool de conexiones, así que crearlos en bucle significa crear pools aislados en vez de reutilizar los existentes. La pregunta correcta no es "¿he hecho Dispose?", sino "¿estoy creando un pool nuevo cuando podía reutilizar uno?".
De estático a factory
El primer arreglo es un HttpClient estático compartido: reutiliza conexiones y elimina buena parte del problema. Pero trae un efecto secundario: al vivir para siempre, se queda con la resolución DNS que obtuvo al crearse. Si la API destino cambia de IP por un despliegue, un failover, un escalado o una migración de infraestructura, el cliente seguirá llamando a la dirección vieja.
Microsoft recomienda IHttpClientFactory precisamente para eso: gestiona la vida del handler por debajo, de modo que se rota cada cierto tiempo y las conexiones se reutilizan sin quedarse ancladas a un DNS obsoleto. La documentación de .NET también señala de forma explícita que los puertos TCP no se liberan de inmediato al cerrar la conexión y que un ritmo alto de peticiones puede agotarlos.
Más allá del caso concreto, el patrón se reconoce en cualquier tecnología: métricas de aplicación planas, un recurso del sistema que se acumula y un reinicio que lo limpia. Cuando aparece ese tercer síntoma, deja de mirar el código de negocio y mira los recursos de red del proceso.
