Save 5% every month: use code 5OFFSTORM at checkout
Home / Guides / C# HttpClient

How to use a proxy with C# HttpClient

Create a SocketsHttpHandler (or HttpClientHandler) with Proxy = new WebProxy("http://GATEWAY_IP:PORT") and UseProxy = true, pass it to one shared HttpClient, and reuse that client. Set the proxy before the first request: .NET locks handler settings once a request has been sent.

Updated October 2026Code & scraping frameworks

C# developers usually meet proxies in one of three places: a console scraper, an ASP.NET service that calls external sites, or a background worker that checks prices. In all three, HttpClient does the work and the proxy lives on the handler underneath it. The setup is four lines. The trouble comes later: the proxy is set on a handler that already sent a request and .NET throws, a Parallel.ForEach opens 300 connections on a 40-thread plan, or the exit IP never changes because one pooled connection carries every request.

This guide covers WebProxy on both handlers, credentials for dedicated proxies, the global HttpClient.DefaultProxy and environment variables, a real cap on parallel requests, how PooledConnectionLifetime and Connection: close decide when your IP changes, IHttpClientFactory in ASP.NET Core, and the exception messages you’ll see when something is off.

Which Storm plan fits

For a .NET scraper or price checker, start with rotating proxies: the Main gateway gives each new connection a new IP and the 40-thread plan ($39/mo) runs 40 parallel requests with no bandwidth bill. If your app signs in and must keep one IP for minutes, a residential port holds its IP for 5 minutes. Pick dedicated proxies when the app runs from Azure Functions or another place with changing IPs, because they accept username and password.

Rotating proxies (from $14/mo): 700,000+ IPs behind fixed gateway IP:PORTs. New IP on every request, or every 3 or 15 minutes. USA, EU, USA+EU or Worldwide. Unlimited bandwidth on every plan.

Get 40 threads for $39/mo See all rotating proxies plans

Before you start

  1. Log in to the member area and copy your gateway IP:PORTs. They never change; the rotation happens on our side.
  2. Add the public IP of the computer or server that will run your tool under Authorized IPs, click Save, and allow up to 15 minutes before testing. Rotating and residential proxies use IP authentication, so there is no username or password.
  3. Dedicated proxies work with either IP authentication or a username and password. Use user:pass if your IP changes or the tool runs on several machines.
  4. Count your threads: the tool’s total open connections must stay within your plan (for example 40 threads on the 40-thread plan).

Set up a proxy in HttpClient, step by step

  1. Authorize the machine that runs your app

    Find the public IP of the PC or server that runs the .NET process (open https://api.ipify.org from it), add it under Authorized IPs in the member area, click Save and allow up to 15 minutes. Running from Visual Studio on your desk and from a server later means two IPs to authorize.

  2. Create a WebProxy with an http:// address

    new WebProxy("http://GATEWAY_IP:PORT"). Keep the http scheme even though the sites you call are https: .NET sends a CONNECT request to the gateway and runs TLS with the site inside that tunnel. Set BypassProxyOnLocal = true if the same client also calls local services.

  3. Attach it to the handler, not the client

    On .NET 6 and later use SocketsHttpHandler with Proxy, UseProxy = true and MaxConnectionsPerServer. HttpClientHandler has the same Proxy and UseProxy properties and is what .NET Framework projects use. Set everything before the first request; afterwards the handler throws InvalidOperationException on any change.

  4. Create one HttpClient and keep it

    Make the client static or a singleton. A new HttpClient per request opens a new connection pool each time, which leaves sockets in TIME_WAIT and makes your thread count unpredictable.

  5. Verify the exit IP

    Call https://api.ipify.org?format=json and print the result. It should be a pool IP. Run it five times on the Main gateway with ConnectionClose = true and you should get five different IPs.

  6. Limit parallel requests to your threads

    Use Parallel.ForEachAsync with MaxDegreeOfParallelism, or a SemaphoreSlim, set to your plan’s threads. 40 on the 40-thread plan, 10 if the target is Google.

Copy-paste C# examples

Swap GATEWAY_IP:PORT for a gateway from your member area. These examples target .NET 6 or newer; the HttpClientHandler one also compiles on .NET Framework 4.7.2+.

SocketsHttpHandler + WebProxy (recommended)
using System.Net;

var handler = new SocketsHttpHandler
{
    Proxy = new WebProxy("http://GATEWAY_IP:PORT"),   // http:// even for https sites
    UseProxy = true,
    MaxConnectionsPerServer = 40,                     // per target site, see below
    PooledConnectionLifetime = TimeSpan.FromMinutes(2),
    ConnectTimeout = TimeSpan.FromSeconds(15),
};
var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(60) };

string ip = await client.GetStringAsync("https://api.ipify.org?format=json");
Console.WriteLine(ip);   // a pool IP, not yours
HttpClientHandler (.NET Framework and older code)
using System.Net;
using System.Net.Http;

var handler = new HttpClientHandler
{
    Proxy = new WebProxy("http://GATEWAY_IP:PORT"),
    UseProxy = true,
};
var client = new HttpClient(handler);
Dedicated proxy with username and password
using System.Net;

var proxy = new WebProxy("http://PROXY_IP:PORT")
{
    Credentials = new NetworkCredential("USERNAME", "PASSWORD"),
};
var handler = new SocketsHttpHandler { Proxy = proxy, UseProxy = true };
var client = new HttpClient(handler);
// .NET answers the proxy's 407 challenge with these credentials.
40 parallel requests, new IP per request (Main gateway)
using System.Net;

const int Threads = 40;   // your plan's threads minus anything else using it

var client = new HttpClient(new SocketsHttpHandler
{
    Proxy = new WebProxy("http://GATEWAY_IP:PORT"),
    UseProxy = true,
}) { Timeout = TimeSpan.FromSeconds(45) };

var urls = Enumerable.Range(0, 500).Select(i => $"https://httpbin.org/anything/{i}");

await Parallel.ForEachAsync(urls,
    new ParallelOptions { MaxDegreeOfParallelism = Threads },
    async (url, ct) =>
    {
        using var req = new HttpRequestMessage(HttpMethod.Get, url);
        req.Headers.ConnectionClose = true;   // close after this request: next one gets a new IP
        try
        {
            using var res = await client.SendAsync(req, ct);
            Console.WriteLine($"{(int)res.StatusCode} {url}");
        }
        catch (HttpRequestException e)
        {
            Console.WriteLine($"ERR {url}: {e.Message} {e.InnerException?.Message}");
        }
    });
ASP.NET Core: named client with IHttpClientFactory
// Program.cs
using System.Net;

builder.Services.AddHttpClient("storm", c => c.Timeout = TimeSpan.FromSeconds(60))
    .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
    {
        Proxy = new WebProxy("http://GATEWAY_IP:PORT"),
        UseProxy = true,
        MaxConnectionsPerServer = 20,
    })
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));

// In a service:
// public PriceChecker(IHttpClientFactory f) => _http = f.CreateClient("storm");

MaxConnectionsPerServer is not your plan limit

Microsoft defines “server” for MaxConnectionsPerServer as one combination of target host, port, Host header and proxy URI. So the limit applies per website, not per gateway. Set it to 40 and call 25 different domains in parallel, and the handler is allowed up to 1,000 connections through the same gateway. Your plan counts every one of them.

Keep MaxConnectionsPerServer as a per-site politeness limit, and enforce the plan limit on the work itself:

  • Parallel.ForEachAsync with MaxDegreeOfParallelism (shown above) for batch jobs;
  • a shared SemaphoreSlim(40) around SendAsync for long-running services that get work from a queue;
  • in ASP.NET Core, one semaphore registered as a singleton and used by every service that calls through the proxy.

Avoid Parallel.ForEach with .Result or .Wait() inside it: it blocks thread-pool threads and the real number of open requests drifts. The threads guide explains how the gateway counts connections.

PooledConnectionLifetime, Connection: close and IP rotation

On the Main rotating gateway, the exit IP is chosen when a connection (an HTTPS tunnel) opens. SocketsHttpHandler keeps connections in a pool and reuses them, and by default PooledConnectionLifetime is infinite. So a long-running service can send thousands of requests from one IP without you noticing. You choose the behaviour with two settings:

  • A new IP for every request: set request.Headers.ConnectionClose = true. The connection closes after the response and the next request opens a new tunnel.
  • A new IP every so often: leave reuse on and set PooledConnectionLifetime, for example 30 seconds or 2 minutes. Connections older than that are retired after their current request, so the IP turns over on that rhythm while you keep most of the speed of reuse.
  • The same IP for a session: don’t try to stretch a pooled connection. Point a separate client at the 3-minute or 15-minute gateway, which keeps the IP across new connections for that window.

Residential ports work differently: every connection on a port shares its current IP, and the IP changes every 5 minutes at fixed minutes (1, 6, 11 and so on). Pool settings don’t move that schedule. A request that’s in flight at the change can fail, so add a retry (Polly or Microsoft.Extensions.Http.Resilience) for HttpRequestException and 5xx.

Several gateways or ports: one client each

A handler has one Proxy, and you can’t change it after the first request. To spread load over several rotating gateways or residential ports, build one HttpClient per gateway at startup and pick between them with a round-robin counter (Interlocked.Increment, modulo the count). Microsoft’s own guidance says more than one client instance is the right approach when you work with multiple proxies.

A custom IWebProxy whose GetProxy(Uri) returns a different address per call looks tempting, but the handler pools connections per proxy, so the result is hard to predict. Separate clients are clearer and easier to log: you know exactly which gateway or port produced each 403.

Environment variables and HttpClient.DefaultProxy

If you never set Proxy, .NET uses HttpClient.DefaultProxy. It reads HTTP_PROXY, HTTPS_PROXY, ALL_PROXY and NO_PROXY on every platform. On Windows and macOS it falls back to the system proxy settings when those variables are empty; on Linux it uses no proxy.

Handy for containers: docker run -e HTTPS_PROXY=http://GATEWAY_IP:PORT proxies the app without code changes. Two gotchas. NO_PROXY doesn’t accept * wildcards (use a leading dot such as .internal.example). And a forgotten system proxy on a Windows dev box sends traffic somewhere you didn’t expect, so set Proxy explicitly in scraper code. Setting the proxy at OS level is covered in the Windows proxy guide.

Which Storm plan for a .NET app

  • Scraping public pages, price and stock checks: rotating, Main gateway, ConnectionClose on. 40 threads for $39/mo or 80 for $59/mo, unlimited bandwidth. USA, EU, USA+EU, or Worldwide for scraping.
  • Google results: Main gateway and at most a quarter of your threads: 10 parallel requests on the 40-thread plan.
  • Logged-in flows: the 15-minute gateway with a CookieContainer per session, or residential ports (5 ports for $50/mo = 5 IPs at once, up to 50 threads each).
  • Azure Functions, App Service (several possible outbound IPs), GitHub-hosted runners: IP authorization can’t follow those IPs. Use dedicated proxies with NetworkCredential, or run on a VM with a fixed IP and authorize it.

Compare rotating thread plans and residential port plans. The smallest package of each type has a 24-hour money-back guarantee on your first order.

Common errors and fixes

HttpRequestException with Connection reset by peer, forcibly closed by the remote host or The response ended prematurelyThe gateway accepted the connection and reset it: the app’s public IP isn’t authorized yet, or it was saved under 15 minutes ago. Storm rotating and residential gateways don’t answer an unauthorized IP with a 407. Check the IP the server really uses and compare it with Authorized IPs.
SocketException: Connection refused / actively refused itWrong gateway IP or port, or an outbound firewall rule. Copy the gateway again from the member area. This is never an authorization problem.
The proxy tunnel request to proxy '...' failed with status code '407'On dedicated proxies the NetworkCredential is missing or wrong. On rotating or residential gateways a 407 comes from a different proxy: check HTTPS_PROXY and the Windows system proxy.
InvalidOperationException: This instance has already started one or more requestsYou changed Proxy or another handler property after the first request. Configure the handler fully before use, and build a new handler and client for a different proxy.
TaskCanceledException mentioning HttpClient.TimeoutRequests waited too long: too many in flight for the plan, or a slow site. Lower the parallelism and set Timeout explicitly instead of relying on the 100-second default.
SSL error or “scheme not supported” right awayThe proxy address starts with https://. Use http://GATEWAY_IP:PORT; the site is still fetched over HTTPS through the tunnel.
Same IP on every requestConnection pooling. Set ConnectionClose = true per request, or a short PooledConnectionLifetime, and use the Main gateway.

FAQ

Do I need a username and password for Storm rotating proxies in C#?

No. Rotating and residential gateways authorize the IP of the machine running your app, so a plain new WebProxy("http://GATEWAY_IP:PORT") is enough. Only private dedicated proxies take NetworkCredential, and they also work with IP authorization.

How do I change the proxy per request in HttpClient?

You can’t on the same handler; the proxy is fixed once the first request is sent. Create one client per proxy and choose a client per request. With a backconnect gateway you rarely need that: the Main gateway already gives a new IP per connection.

HttpClientHandler or SocketsHttpHandler?

On .NET Core and .NET 5+, HttpClientHandler is a thin wrapper over SocketsHttpHandler. Use SocketsHttpHandler directly when you need PooledConnectionLifetime or ConnectTimeout. On .NET Framework only HttpClientHandler exists.

Does IHttpClientFactory work with proxies?

Yes, through ConfigurePrimaryHttpMessageHandler. The factory recycles handlers after SetHandlerLifetime, so an old and a new handler can briefly overlap; leave a few threads of headroom. Microsoft warns that pooled handlers share cookies, so keep login sessions on their own client.

Can I use RestSharp or Flurl with Storm proxies?

Yes. Both are built on HttpClient, so give them a proxy through their own options or a handler you configure as above. The address (http://GATEWAY_IP:PORT) and the thread cap stay the same.

Can my C# app use Storm proxies from Azure Functions?

Rotating and residential won’t work reliably there, because the outgoing IP changes and those gateways only accept authorized IPs. Use dedicated proxies with username and password, or move the scraping part to a VM with a fixed IP.

Still have questions? Contact us here. A real person answers.

Related guides

Tool facts checked against the official documentation (October 2026): HttpClient guidelines for .NET · SocketsHttpHandler.MaxConnectionsPerServer · SocketsHttpHandler.PooledConnectionLifetime · HttpClient.DefaultProxy · WebProxy class · IHttpClientFactory with .NET. Storm Proxies facts: our plans page and refund policy.

Unlimited bandwidth. One flat monthly price.

Access is live the moment you pay, and the smallest package of each proxy type has a 24-hour money-back guarantee on your first order.