In .NET, the proxy does not live on HttpClient itself. It lives on the message handler underneath it, as a Proxy property that takes an IWebProxy, and the built-in implementation of that interface is the WebProxy class. Once you know that split, the rest falls into place: build a WebProxy, hand it to a handler, hand the handler to an HttpClient. This guide covers that path, authentication with NetworkCredential, SOCKS5 on .NET 6 and later, rotating a pool with a custom IWebProxy, and the verification step that proves the proxy is carrying your traffic.
We run a proxy network and ship .NET tooling against it, so the mistakes below are ones we see constantly: a new HttpClient built per request that exhausts sockets, a proxy set on the client when it belongs on the handler. Every example is copy-paste runnable on .NET 6 or later. Where one needs a live proxy, pull a fresh one from our free proxy API, which returns real, recently checked endpoints without a key.
How do you use a proxy with C#?
Create a WebProxy with the proxy address, assign it to the Proxy property of an HttpClientHandler (or SocketsHttpHandler), and pass that handler into the HttpClient constructor. Set Credentials on the WebProxy for authentication. Every request through that client goes via the proxy.
HttpClient with a WebProxy
The WebProxy class holds the proxy settings that HttpClient uses. Construct one from a URL string, put it on a handler's Proxy property, and turn UseProxy on:
using System;
using System.Net;
using System.Net.Http;
using System.Text.Json;
var proxy = new WebProxy("http://203.0.113.7:8080")
{
Credentials = new NetworkCredential("user", "pass"), // sets Proxy-Authorization
};
var handler = new HttpClientHandler
{
Proxy = proxy,
UseProxy = true,
};
using var client = new HttpClient(handler)
{
Timeout = TimeSpan.FromSeconds(15),
};
string body = await client.GetStringAsync("https://httpbin.org/ip");
Console.WriteLine(body);
For an https:// target, the handler opens a CONNECT tunnel through the proxy and performs TLS end to end with the origin, so a plain HTTP proxy carries your encrypted traffic without seeing inside it. That is why http://proxy:port used against an https:// URL is the normal, correct combination even though the schemes look mismatched.
The one rule to internalize is that HttpClient is meant to be built once and reused, per Microsoft's own guidance, because each instance owns a connection pool and creating them per request leaks sockets. Store the client in a static field or register it through IHttpClientFactory, and never wrap it in a using that runs on every call in real code.
SocketsHttpHandler: the modern handler
Since .NET Core 2.1, HttpClientHandler is a compatibility shim over SocketsHttpHandler, the cross-platform stack that actually opens the sockets. For new code, using SocketsHttpHandler directly gives you the same Proxy property plus real control over connection pooling:
var handler = new SocketsHttpHandler
{
Proxy = proxy,
UseProxy = true,
PooledConnectionLifetime = TimeSpan.FromMinutes(2), // recycle sockets, useful when rotating
};
using var client = new HttpClient(handler);
PooledConnectionLifetime matters for long-running services: it caps how long a pooled connection (and therefore a given proxy exit) is reused, which keeps a rotating setup honest instead of pinning every request to the first exit that answered.
Proxy authentication
Paid proxies authenticate with a username and password, and in .NET that is the Credentials property on the WebProxy, set to a NetworkCredential:
var proxy = new WebProxy("http://203.0.113.7:8080")
{
Credentials = new NetworkCredential("user", "pass"),
};
If you are relying on the machine's default proxy rather than your own WebProxy, the credentials go on the handler's DefaultProxyCredentials property instead, which only applies when UseProxy is true and Proxy is left null. Either way, a rejected login returns HTTP 407 Proxy Authentication Required, which tells you the proxy is reachable and only the credentials are wrong. We cover that status in how to fix 407 Proxy Authentication Required.
SOCKS5 proxies
.NET added SOCKS proxy support in .NET 6. You do not need a different API: you give WebProxy a SOCKS URI and the SocketsHttpHandler handles the rest. The socks4, socks4a, and socks5 schemes are all recognized:
var handler = new SocketsHttpHandler
{
Proxy = new WebProxy("socks5://198.51.100.14:1080"),
UseProxy = true,
};
using var client = new HttpClient(handler);
Console.WriteLine(await client.GetStringAsync("https://httpbin.org/ip"));
If the SOCKS5 proxy requires a username and password, attach a NetworkCredential to the WebProxy.Credentials the same way you would for an HTTP proxy. On .NET Framework and on .NET Core before 6, there is no built-in SOCKS support at all, so that is your cue to upgrade the target framework rather than hunt for a workaround. For the difference between local and proxy-side DNS that the SOCKS schemes control, see what is a SOCKS5 proxy.
Rotating across a pool
One IP sending every request is the exact shape a rate limiter watches for, and the clean way to rotate in .NET is not a pile of HttpClients but a single client whose proxy changes per request. IWebProxy is an interface, so implement it: GetProxy is called once per request, and you return a random exit each time:
using System;
using System.Net;
sealed class RotatingProxy : IWebProxy
{
private readonly Uri[] _pool;
public RotatingProxy(params string[] pool) =>
_pool = Array.ConvertAll(pool, u => new Uri(u));
public ICredentials? Credentials { get; set; }
// Called per request: hand back a different exit each time.
public Uri GetProxy(Uri destination) => _pool[Random.Shared.Next(_pool.Length)];
public bool IsBypassed(Uri host) => false;
}
Wire that one instance into a single reused handler and you get rotation without touching socket hygiene:
var handler = new SocketsHttpHandler
{
Proxy = new RotatingProxy(
"http://203.0.113.7:8080",
"http://203.0.113.24:3128",
"http://198.51.100.14:8080"),
UseProxy = true,
PooledConnectionLifetime = TimeSpan.FromSeconds(30),
};
using var client = new HttpClient(handler);
Console.WriteLine(await client.GetStringAsync("https://httpbin.org/ip"));
You do not have to hardcode the list either. Our free proxy API returns a fresh pool as plain text you can pull at startup and splat into the constructor:
string raw = await new HttpClient().GetStringAsync(
"https://hproxy.com/api/proxy-list?format=txt&protocol=http&recent=true&limit=50");
string[] pool = raw
.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(p => $"http://{p}")
.ToArray();
var rotating = new RotatingProxy(pool);
For production, a rotating gateway that gives you one endpoint and a fresh residential IP per request removes list management entirely, but rotating a real list first is the best way to understand what that gateway does for you.
Verify the exit IP
Never assume a proxy is working. Confirm the target sees the proxy's address and not yours, and do it before you trust an exit with real traffic:
using System.Text.Json;
static async Task<string> ExitIp(HttpClient c)
{
string json = await c.GetStringAsync("https://httpbin.org/ip");
return JsonDocument.Parse(json).RootElement.GetProperty("origin").GetString()!;
}
using var direct = new HttpClient();
using var proxied = new HttpClient(new SocketsHttpHandler
{
Proxy = new WebProxy("http://203.0.113.7:8080"),
UseProxy = true,
});
string real = await ExitIp(direct);
string viaProxy = await ExitIp(proxied);
Console.WriteLine($"real: {real}");
Console.WriteLine($"proxy: {viaProxy}");
if (real == viaProxy)
throw new InvalidOperationException("proxy is not changing your IP");
If the two IPs match, the request never went through the proxy, usually because UseProxy was left false or the destination fell into the bypass list. It is also worth checking what headers arrive at https://httpbin.org/headers, because a transparent proxy can forward your real address in X-Forwarded-For even while origin looks changed. Our proxy checker runs that whole battery, exit IP, anonymity grade, real geolocation, and latency, in one paste if you would rather not script it.
Where to go from here
Two habits keep .NET proxy code alive unattended: set HttpClient.Timeout (and reuse one client) so a silent proxy cannot hang a request or leak sockets, and verify every exit before you trust it. For a live pool to test against, the free proxy list re-verifies its entries every few minutes, and the free API hands them to you with no key.
From here, the cURL guide is the fastest way to sanity-check a proxy from the shell before you wire it into C#, the Python requests guide is a close sibling if your stack spans both, and proxies for web scraping covers choosing the right proxy type and the request hygiene no HTTP client can add for you. When a project graduates from experiments to production, get IPs nobody else is burning on our paid pools.
Sources
- Microsoft Learn: WebProxy class: the constructors, the
Address,Credentials,BypassProxyOnLocal, andBypassListproperties, and the worked example of building anHttpClientaround aWebProxyviaHttpClientHandler.Proxy. - Microsoft Learn: HttpClientHandler class: the
Proxy,UseProxy, andDefaultProxyCredentialsproperties, and the note that since .NET Core 2.1 the handler is backed bySocketsHttpHandler. - .NET 6 networking improvements (Microsoft DevBlogs): SOCKS proxy support added in .NET 6, the
socks4/socks4a/socks5schemes, and thenew WebProxy("socks5://...")form assigned to a handler'sProxy.