If you have ever encountered a SocketException: Address already in use or noticed that a .NET application suddenly runs out of available sockets after a few hours of working with proxies, the issue is almost always due to improper use of HttpClient. In this article, we will discuss how to configure a proxy via SocketsHttpHandler, connect SOCKS5, and organize IP rotation so that the connection pool does not become a bottleneck.
Why HttpClient "consumes" sockets
A classic mistake is to create a new instance of HttpClient for each request within a using statement. When working with proxies, this issue is exacerbated: each connection through a proxy server requires a separate TCP handshake, and if you are also changing proxies on the fly, the connection pool is not reused, and sockets linger in the TIME_WAIT state.
On Windows, this is particularly critical — by default, a limited number of dynamic ports are available (usually 16384), and if the application is parsing data through a proxy with a high request intensity, all ports can be exhausted in just a few minutes. On Linux, the situation is somewhat more lenient due to kernel settings, but the problem persists if connections are not reused.
The old HttpClientHandler, available since the days of .NET Framework, does not provide flexible control over the connection lifecycle. That is why starting from .NET Core 2.1, it is recommended to use SocketsHttpHandler — it is written entirely in managed code and provides direct access to the connection pool, DNS timeouts, and proxy settings.
SocketsHttpHandler: Why is it needed
SocketsHttpHandler is the transport layer on which HttpClient is built by default on Linux and macOS since .NET Core 2.1, and starting from .NET 5 — also on Windows. Unlike the old WinHttpHandler, it:
- allows you to set
PooledConnectionLifetime— the lifetime of a connection in the pool, which is critical when working with rotating proxies; - provides access to
IWebProxythrough the delegateProxy, which can be overridden programmatically on the fly; - supports
ConnectCallback— a low-level hook for overriding the transport, which is necessary for SOCKS5; - correctly releases sockets after
PooledConnectionIdleTimeoutexpires, leaving no garbage in the system.
The basic setup looks like this:
var handler = new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(5),
PooledConnectionIdleTimeout = TimeSpan.FromSeconds(30),
MaxConnectionsPerServer = 50,
AutomaticDecompression = DecompressionMethods.All
};
var client = new HttpClient(handler)
{
Timeout = TimeSpan.FromSeconds(30)
};
The main rule: HttpClient with this handler should be a singleton or created via IHttpClientFactory — do not recreate it for each request, otherwise the whole point of the connection pool is lost.
Configuring a proxy via SocketsHttpHandler
For HTTP/HTTPS proxies, it is sufficient to set the Proxy property at the handler level. This works for datacenter proxies and residential proxies that provide access via the HTTP CONNECT protocol:
var proxy = new WebProxy("http://proxy-host:8000")
{
Credentials = new NetworkCredential("login", "password")
};
var handler = new SocketsHttpHandler
{
Proxy = proxy,
UseProxy = true,
PooledConnectionLifetime = TimeSpan.FromMinutes(3)
};
var client = new HttpClient(handler);
var response = await client.GetAsync("https://example.com");
Console.WriteLine(await response.Content.ReadAsStringAsync());
If you need to connect different proxies for different hosts (for example, authentication to one domain goes directly, while scraping goes through a proxy), use a delegate instead of a static object:
var handler = new SocketsHttpHandler
{
Proxy = new WebProxy(),
UseProxy = true
};
handler.Proxy = new CustomProxy(uri =>
{
return uri.Host.Contains("target-site")
? new Uri("http://proxy-host:8000")
: null;
});
For tasks like parsing marketplaces — for example, monitoring prices on Wildberries or Ozon — it is common to combine both approaches: the main traffic goes through datacenter proxies as datacenter proxies, while requests to captcha services or authentication go directly.
SOCKS5 support in .NET
Starting from .NET 6, SocketsHttpHandler received native support for SOCKS4/SOCKS4a/SOCKS5 by specifying the scheme in the proxy URI — no third-party libraries are required:
var handler = new SocketsHttpHandler
{
Proxy = new WebProxy(new Uri("socks5://proxy-host:1080"))
{
Credentials = new NetworkCredential("login", "password")
},
UseProxy = true
};
var client = new HttpClient(handler);
var response = await client.GetAsync("https://httpbin.org/ip");
An important nuance: before .NET 6, such capability did not exist, and developers had to write their own ConnectCallback that manually performed the SOCKS5 handshake before establishing a TLS connection. If your project is still on .NET Core 3.1 or .NET 5, this path is still relevant:
var handler = new SocketsHttpHandler
{
ConnectCallback = async (context, cancellationToken) =>
{
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp)
{
NoDelay = true
};
await socket.ConnectAsync("proxy-host", 1080, cancellationToken);
await PerformSocks5Handshake(socket, context.DnsEndPoint, cancellationToken);
return new NetworkStream(socket, ownsSocket: true);
}
};
In practice, for production projects, it is easier to upgrade to .NET 6+ and use the built-in implementation — it is covered by runtime tests and does not require self-support for the handshake protocol.
Proxy rotation without recreating HttpClient
A common mistake when implementing IP rotation is creating a new HttpClient (and thus a new handler and a new connection pool) for each proxy change. This inevitably leads to the accumulation of unclosed sockets. The correct approach is to implement your own IWebProxy that returns a different proxy address on each call:
public class RotatingProxy : IWebProxy
{
private readonly string[] _proxies;
private int _index = -1;
public RotatingProxy(string[] proxies) => _proxies = proxies;
public ICredentials Credentials { get; set; }
public Uri GetProxy(Uri destination)
{
var next = Interlocked.Increment(ref _index) % _proxies.Length;
return new Uri(_proxies[next]);
}
public bool IsBypassed(Uri host) => false;
}
This class connects to the same singleton SocketsHttpHandler:
var handler = new SocketsHttpHandler
{
Proxy = new RotatingProxy(new[]
{
"http://proxy1:8000",
"http://proxy2:8000",
"http://proxy3:8000"
}),
UseProxy = true,
PooledConnectionLifetime = TimeSpan.FromSeconds(60)
};
var client = new HttpClient(handler);
This scheme reuses the internal socket pool, but each request indeed goes through a new IP. If you are building parallel connection pools for each proxy (for speed), set a reasonable PooledConnectionLifetime — usually 30-90 seconds, so that connections do not expire faster than the IP on the proxy provider's side. For tasks like parsing Avito or collecting data from the Instagram API, it is more convenient to use residential proxies with rotation on the provider's side — then in the code, you don't need to change the address at all, just keep one "entry point" with automatic outgoing IP replacement.
PooledConnectionLifetime and DNS rotation
If the proxy provider uses DNS rotation (one hostname resolves to different IPs with each request), it is critical to set a limited PooledConnectionLifetime. By default, it is set to Infinite, and .NET will keep the connection to the old IP indefinitely, ignoring DNS updates.
var handler = new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromSeconds(30), // recreate the connection
PooledConnectionIdleTimeout = TimeSpan.FromSeconds(15),
MaxConnectionsPerServer = 20
};
This is especially important for mobile proxies, where the IP change by the provider can occur every 1-10 minutes as part of the mobile operator's rotation. If you keep the connection longer, requests will continue to go through an already released IP, and sometimes even receive a timeout error. For such scenarios, mobile proxies are well-suited — they provide the highest trust from anti-fraud systems but require careful configuration of connection TTLs specifically at the client level.
A short PooledConnectionLifetime is not free — each recreation of a connection means a new TCP+TLS handshake, which adds a delay of 50-200 ms. The balance between IP freshness and performance is determined empirically: for price monitoring tasks, 60-120 seconds is sufficient, while for high-frequency account farming, sometimes 10-20 seconds is needed.
Comparison of proxy types for .NET applications
| Proxy Type | Protocol | Recommended PooledConnectionLifetime | Tasks |
|---|---|---|---|
| Datacenter Proxies | HTTP/HTTPS, SOCKS5 | 5-10 minutes | Parsing, API integrations, high speed |
| Residential Proxies | HTTP/HTTPS, SOCKS5 | 30-90 seconds | Price monitoring, bypassing geo-blocks |
| Mobile Proxies | HTTP/HTTPS, SOCKS5 | 10-30 seconds | Social media automation, anti-fraud sensitive APIs |
IHttpClientFactory and typed clients with proxies
In ASP.NET Core projects, it is recommended to register HttpClient through IHttpClientFactory — this relieves the developer of the responsibility for managing the lifecycle of handlers and automatically reuses internal SocketsHttpHandler.
builder.Services.AddHttpClient("ProxyClient")
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
Proxy = new WebProxy(new Uri("socks5://proxy-host:1080"))
{
Credentials = new NetworkCredential("login", "password")
},
UseProxy = true,
PooledConnectionLifetime = TimeSpan.FromMinutes(2)
})
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
Note the SetHandlerLifetime — this is a separate mechanism for rotating the handlers themselves within the factory (default 2 minutes), which should not be confused with PooledConnectionLifetime of an individual connection. The factory periodically creates a new handler, but old connections continue to live out their time until active requests are completed — the garbage collector will clean them up itself.
For scenarios with multiple proxy pools, it is convenient to register several named clients — for example, separately for the US and separately for Europe, if the task is to test Google Ads or Facebook Ads from different regions:
builder.Services.AddHttpClient("US-Proxy")
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
Proxy = new WebProxy("http://us-proxy-host:8000"),
UseProxy = true
});
builder.Services.AddHttpClient("EU-Proxy")
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
Proxy = new WebProxy("http://eu-proxy-host:8000"),
UseProxy = true
});
Diagnosing socket leaks
If you suspect socket accumulation, do not guess — check. On Linux, a quick way to see the number of open connections for a process:
# number of connections in TIME_WAIT
ss -tan state time-wait | wc -l
# all connections of a specific process (PID)
ls -la /proc/{PID}/fd | grep socket | wc -l
On Windows, similar statistics can be obtained with the command netstat -ano filtered by the process PID. For deeper diagnostics at the .NET level, use the built-in tool dotnet-counters, which shows System.Net.Http metrics in real time:
dotnet-counters monitor -p {PID} --counters System.Net.Http
# key counters:
# current-connections
# connections-duration
# requests-failed-rate
If current-connections grows monotonically and never decreases — this is a direct sign that the handler is being recreated too often, or PooledConnectionIdleTimeout is set to Infinite, and requests stop completing correctly (for example, the response body is not fully read, and the connection does not return to the pool).
Another common cause of leaks is a forgotten response.Content that was not read or disposed of. Always use using var response = ... or an explicit call to Dispose() after finishing working with the response.
Conclusion
Proper work with proxies in .NET is based on three principles: use SocketsHttpHandler instead of outdated implementations, keep HttpClient as a singleton (or via IHttpClientFactory), and implement proxy rotation through a custom IWebProxy, not by recreating the entire client. Starting from .NET 6, SOCKS5 is natively supported, eliminating the need to write your own handshake protocol.
If your application scrapes marketplaces or collects data via APIs with high request intensity, consider datacenter proxies — they provide maximum speed with proper connection pool configuration. For tasks where anonymity and similarity to regular user traffic are important, residential proxies are better suited, and for working with mobile APIs and social platforms — mobile proxies with short connection lifetimes in the code.