C# · setup guide

C# HttpClient proxy setup

A C# HttpClient proxy lives on the handler: a WebProxy for the gateway with a NetworkCredential for the login, set on SocketsHttpHandler or HttpClientHandler. .NET wants one long-lived client, which pools connections, so rotation comes down to how long a pooled connection may live.
You will need

Residentialresi.proxymonkey.io:8000

ISP or datacenterIP:PORT

CredentialsUSER:PASS

Copy your own from the dashboard, which also lists the host and port for every order. Where it differs from this page, the dashboard is right.

Install

.NET 8 or newer. Everything here is in the base library; a console project is enough to try it.

terminal
dotnet new console -n MonkeyProxy

Rotating residential

PooledConnectionLifetime = TimeSpan.Zero stops the pool reusing a connection, so every request through this one client opens a new tunnel through the residential gateway and gets a new address. The client itself is still created once, which avoids running out of local ports.

Program.cs
using System.Net;

var handler = new SocketsHttpHandler
{
    Proxy = new WebProxy("http://resi.proxymonkey.io:8000")
    {
        Credentials = new NetworkCredential("USER", "PASS"),
    },
    PooledConnectionLifetime = TimeSpan.Zero,
    ConnectTimeout = TimeSpan.FromSeconds(10),
};
using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(30) };

for (var i = 0; i < 3; i++)
{
    Console.WriteLine(await client.GetStringAsync("https://httpbin.org/ip"));
}

A static datacenter or ISP IP

On a static datacenter IP, let the pool reuse connections; a lifetime of a few minutes still recycles them now and then. For an ISP order, use the host and port from your dashboard.

Program.cs
using System.Net;

var handler = new SocketsHttpHandler
{
    Proxy = new WebProxy("http://IP:PORT")
    {
        Credentials = new NetworkCredential("USER", "PASS"),
    },
    PooledConnectionLifetime = TimeSpan.FromMinutes(5),
    ConnectTimeout = TimeSpan.FromSeconds(10),
};
using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(30) };

foreach (var url in new[] { "https://httpbin.org/ip", "https://httpbin.org/headers" })
{
    using var response = await client.GetAsync(url);
    var body = await response.Content.ReadAsStringAsync();
    Console.WriteLine($"{(int)response.StatusCode} {body[..Math.Min(80, body.Length)]}");
}

Keeping one identity

With a normal pool lifetime, requests to one host reuse a tunnel and keep its exit address until the connection is recycled or idles out (PooledConnectionIdleTimeout). Cookies live in the handler's CookieContainer, shared by everything sent through that client. For two identities, use two handlers; for a residential address that holds across new connections, use a sticky session, whose setting for your account is in the dashboard.

Specific to C# HttpClient

Things worth knowing

C# proxy authentication goes on the WebProxy

handler.Credentials is for the target site. Proxy credentials belong on WebProxy.Credentials, or on DefaultProxyCredentials if you rely on the system or environment proxy. On .NET Framework, HttpClientHandler takes the same WebProxy.

Handler.cs
var handler = new HttpClientHandler
{
    Proxy = new WebProxy("http://resi.proxymonkey.io:8000")
    {
        Credentials = new NetworkCredential("USER", "PASS"),
    },
    UseProxy = true,
};

Swap proxies with a new handler, not a new setting

A handler's properties are frozen after its first request, and changing Proxy then throws. Build one handler and client per proxy, keep each for the life of the app, and choose between them per request.

Add up headers and body for the meter

We meter request bytes plus response bytes, headers included. SocketsHttpHandler does not decompress unless you set AutomaticDecompression, so the byte array is the body as sent. The sum leaves out the request and status lines and TLS overhead, so our figure will be slightly higher.

Bytes.cs
using var response = await client.GetAsync("https://example.com/");
var body = await response.Content.ReadAsByteArrayAsync();

long Head(IEnumerable<KeyValuePair<string, IEnumerable<string>>> headers) =>
    headers.Sum(h => h.Key.Length + string.Join(", ", h.Value).Length + 4);

var sent = Head(response.RequestMessage!.Headers);
var received = Head(response.Headers) + Head(response.Content.Headers) + body.Length;
Console.WriteLine($"sent ~{sent} B, received ~{received} B");
When it breaks

Common errors and fixes

  • HttpRequestException: The proxy tunnel request to proxy ... failed with status code '407'

    Why
    The gateway turned down the CONNECT: wrong credentials, or credentials set on the handler instead of the WebProxy.
    Fix
    Put a NetworkCredential on WebProxy.Credentials and copy the pair fresh from the dashboard.
  • TaskCanceledException: The request was canceled due to the configured HttpClient.Timeout of 30 seconds elapsing

    Why
    No response within HttpClient.Timeout, which defaults to 100 seconds.
    Fix
    Set ConnectTimeout on the handler and a shorter Timeout on the client, catch the exception and retry. With PooledConnectionLifetime at zero on the rotating gateway, the retry leaves from a new address.
  • HttpRequestException: The SSL connection could not be established

    Why
    The inner AuthenticationException has the detail, usually a target certificate your machine does not trust. The proxy passes TLS through the tunnel.
    Fix
    Install the missing CA in the system store. A validation callback that always returns true is for testing only.
  • InvalidOperationException: This instance has already started one or more requests

    Why
    A property such as Proxy was changed on a handler after it had sent a request.
    Fix
    Configure the handler fully before the first request, and use a separate handler per proxy.

A failed connection that moved no data is not billed. Retries you send are billed like any other request, and each shows as its own line in your usage log.

The community layer

C# HttpClient still misbehaving?

Paste the error and the few lines that set up the proxy into Discord, with the password taken out. Someone there has seen it before.

Join the Discord

4,200+monkeys in the Discord

  • Help from humans

    Post your error, get an answer. Usually in minutes, usually from someone who has hit the same wall.

  • A status bot that tells on us

    Pool health, incidents and maintenance posted automatically. Including the bad days.

  • Deals and free traffic

    Bonus GB drops, early access to new pools, and the occasional giveaway for a good bug report.

Join the Discord4,200+ monkeys, free to lurk