Guide

How to Use Proxies With Go (net/http and Colly)

Set Transport.Proxy to http.ProxyURL and the client is proxied. SOCKS5 needs no extra package, and here is what NO_PROXY really matches.

HProxy Team··Updated September 16, 2026·13 min read
HProxy.Guide

Free proxies won't hold up here.

Shared datacenter IPs get flagged and dropped fast. When it has to hold, gaming, streaming, accounts, you need mobile and residential IPs that read as a real device, from $0.44/GB, pay as you go.

Proxies for Web Scraping

Set the Proxy field on an http.Transport to http.ProxyURL(parsed). Give that Transport to an http.Client, and every request from that client goes through the proxy. The proxy belongs to the Transport, not to a request. The smallest unit you can proxy is a client, not a call. SOCKS5 goes in the same field: current Go takes a socks5:// URL there, with no extra package.

That last sentence is the one most guides get wrong, so everything below was measured rather than repeated. The runs used Go 1.24.2 on 16 September 2026, against stub proxies on this machine, and the program is linked at the end.

One disambiguation first, because the words collide. GOPROXY and proxy.golang.org are the Go module proxy. That is where the go command downloads modules from, described in the Go Modules Reference. This page is about the other kind, routing your own program's HTTP requests through a proxy server.

Go's standard library has proxy support built in and it is genuinely clean, which is rare. The catches are elsewhere: the proxy lives on the http.Transport rather than on a request, a custom Transport silently drops the environment handling you got for free, and the environment is read once per process. This guide covers the standard-library way, SOCKS5 without an extra package, authentication, what your proxy variables really match, rotating a pool with Colly, and the verification step that proves the proxy is carrying your traffic.

We run a proxy network and write a fair amount of Go against it, so the mistakes below are ones we have made ourselves: a scraper that quietly went direct because a custom Transport forgot its Proxy field, a SOCKS setup that would not compile until the dialer was asserted to the right interface. Every example is a complete, runnable program. 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 Go?

Build an http.Transport with its Proxy field set to http.ProxyURL(parsed), attach that Transport to an http.Client, and every request through that client goes via the proxy. For environment-driven proxying use http.ProxyFromEnvironment. For SOCKS5, pass a socks5:// URL to the same Proxy field. The proxy is a property of the client's Transport, not of any single request.

The proxy lives on the Transport, and the client carries the Transport
  1. http.Client

    your requests

  2. Transport.Proxy

    ProxyURL

  3. Proxy

    adds auth, exits

  4. Target

    sees the exit IP

Source: One proxied client, reused for every request

What Go's proxy support does not do

Four limits are worth knowing before you build on any of this.

There is no per-request proxy field. Proxy sits on the Transport and is a function called once per request, so you can branch inside it on the *http.Request. You cannot attach a proxy to a request itself, which means switching exits usually means switching clients.

SOCKS is only in the Proxy field for HTTP work. Transport.Proxy takes a socks5:// URL, which covers requests made by an http.Client. A connection that is not HTTP still needs a dialer, and that is what golang.org/x/net/proxy is for.

A Transport you build yourself ignores the environment. http.DefaultTransport sets ProxyFromEnvironment for you and a Transport you construct does not, which is the trap two sections below. We checked both: the default carries a Proxy function, and &http.Transport{} carries none.

The environment is read once per process. Setting HTTP_PROXY with os.Setenv after your program has already made a request changes nothing at all, which the measured section covers.

Nothing tests an exit before using it. Colly's round-robin switcher hands out the next entry in order rather than checking it, so a dead proxy in the list fails that request instead of being skipped. Verification is your job, and the last section shows the check.

What does the Transport.Proxy field do?

http.Transport has a Proxy field with the signature func(*http.Request) (*url.URL, error). It is called once per request to decide which proxy, if any, to use. The standard library ships two functions that fit that shape: http.ProxyURL, which always returns the same fixed proxy, and http.ProxyFromEnvironment, which reads the environment. For a single known proxy, http.ProxyURL is all you need:

package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"time"
)

func main() {
	proxyURL, err := url.Parse("http://user:pass@203.0.113.7:8080")
	if err != nil {
		panic(err)
	}

	transport := &http.Transport{
		Proxy: http.ProxyURL(proxyURL),
	}
	client := &http.Client{
		Transport: transport,
		Timeout:   15 * time.Second,
	}

	resp, err := client.Get("https://httpbin.org/ip")
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	fmt.Println(string(body))
}

Two things are quietly doing the right thing here. For an https:// target, the Transport 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. And the user:pass in the proxy URL is not decorative: net/http reads the userinfo and sets the Proxy-Authorization header for you, so authenticated HTTP proxies need no extra code.

Why does HTTP_PROXY stop working on a custom Transport?

http.DefaultTransport, the Transport a plain http.Get uses, already has Proxy: http.ProxyFromEnvironment set. That is why an ordinary Go program respects HTTP_PROXY, HTTPS_PROXY and NO_PROXY with no code at all. Per the net/http documentation, ProxyFromEnvironment matches the request scheme to the right variable, reads either a full URL or a bare host[:port] (assuming http when the scheme is missing), and returns no proxy for localhost.

The trap is what happens the moment you build your own Transport, which you eventually will, for a custom timeout or TLS config. A Transport you construct has an empty Proxy field, so it ignores the environment completely. If you want the environment respected on a custom Transport, say so explicitly:

transport := &http.Transport{
	Proxy: http.ProxyFromEnvironment, // restore env handling on a custom Transport
	// ... your other settings ...
}

This one line is the fix for the surprisingly common "my HTTP_PROXY stopped working after I customized the client" report.

What do my proxy variables actually match?

We asked http.ProxyFromEnvironment directly, once per environment, in a fresh process each time. No request was sent anywhere. Each column is the answer for one address:

Environmenthttp://example.comhttps://example.comhttp://api.example.comhttp://127.0.0.1:8080
HTTP_PROXY setproxydirectproxydirect
http_proxy set, lower caseproxydirectproxydirect
HTTPS_PROXY setdirectproxydirectdirect
both setproxyproxyproxydirect
HTTP_PROXY + NO_PROXY=example.comdirectdirectdirectdirect
HTTP_PROXY + NO_PROXY=.example.comproxydirectdirectdirect
HTTP_PROXY + NO_PROXY=*directdirectdirectdirect

Four rules fall out of that table, and three of them surprise people.

HTTP_PROXY has nothing to do with an https:// address. The variable must match the scheme of the request. Setting one and testing the other is the most common false alarm.

A bare domain in NO_PROXY covers its subdomains, and a leading dot does not cover the domain. The documentation of the httpproxy package says it in one line: foo.com matches foo.com and bar.foo.com, while .y.com matches x.y.com but not y.com. The same page documents an IP prefix and CIDR notation as values, and a single asterisk to turn proxying off.

Loopback is never proxied. A target on localhost or 127.0.0.1 goes direct whatever you set, which is why your proxy looks broken when you test it against a local server.

Lower case wins. When both HTTP_PROXY and http_proxy are set, the documentation says the lower-case value is the one used.

And one rule about time rather than matching:

The environment is read once per process. In our run, a program that asked before the variable was set and again after setting it got the same answer both times: no proxy. A program that set the variable first got the proxy. So os.Setenv("HTTP_PROXY", ...) in the middle of a program does nothing for the requests that follow. Pass the proxy to http.ProxyURL if you need to decide at runtime.

Does Go support SOCKS5 proxies?

Yes, and the answer changed. The documentation of Transport.Proxy in Go 1.24.2 says it plainly:

The proxy type is determined by the URL scheme. "http", "https", "socks5", and "socks5h" are supported. If the scheme is empty, "http" is assumed. "socks5" is treated the same as "socks5h".

So a SOCKS5 proxy needs no extra package and no dialer. It is the same field:

proxyURL, _ := url.Parse("socks5://user:pass@203.0.113.7:1080")
client := &http.Client{
	Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)},
	Timeout:   15 * time.Second,
}

We put that through a stub SOCKS5 proxy on 16 September 2026 and watched what the client sent. Three things are worth knowing:

  • With a proxy that wants no login, the client offered the no-authentication method and the request went through.
  • With user:pass in the URL, it offered the user name and password method as well and completed the exchange, so credentials work here too.
  • Asked for a target named by host name, it sent the name itself to the proxy rather than an address. The lookup happens at the proxy, which is the behaviour curl calls socks5h, and it is why the documentation says the two schemes are the same thing. We cover why remote lookups matter in what is a SOCKS5 proxy.

The advice to leave net/http for SOCKS was once correct: issue 18508, opened on 4 January 2017, is about proxy environment variables not understanding socks5:// at all. It carries the Go1.9Early milestone and is closed, and guides written before that are still handing out the old workaround.

When you still want x/net/proxy

For anything that is not an http.Client, the field above cannot help you, because there is no Transport. A raw TCP connection, a database driver, an SSH client: those need a dialer, and golang.org/x/net/proxy is the package for it. proxy.SOCKS5(network, address, *Auth, forward) returns a Dialer. To wire it into an http.Transport you assert that dialer to proxy.ContextDialer and hand its DialContext to the Transport:

package main

import (
	"fmt"
	"io"
	"net/http"
	"time"

	"golang.org/x/net/proxy"
)

func main() {
	auth := &proxy.Auth{User: "user", Password: "pass"}
	dialer, err := proxy.SOCKS5("tcp", "203.0.113.7:1080", auth, proxy.Direct)
	if err != nil {
		panic(err)
	}

	// The SOCKS5 dialer implements ContextDialer; assert to use DialContext.
	ctxDialer, ok := dialer.(proxy.ContextDialer)
	if !ok {
		panic("SOCKS5 dialer is not a ContextDialer")
	}

	transport := &http.Transport{DialContext: ctxDialer.DialContext}
	client := &http.Client{Transport: transport, Timeout: 15 * time.Second}

	resp, err := client.Get("https://httpbin.org/ip")
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	fmt.Println(string(body))
}

If you would rather parse a URL than pass arguments, proxy.FromURL("socks5://user:pass@203.0.113.7:1080", proxy.Direct) does the same thing from a *url.URL. That package is not in the standard library, so it is a module you install, and we did not run it for this page: the code above follows its documentation, while everything we measured used the standard library alone.

How do I rotate proxies in Colly?

For scraping, most Go code uses Colly, and Colly has proxy rotation built in. proxy.RoundRobinProxySwitcher takes a list of proxy URLs and returns a function you attach with SetProxyFunc; it hands out a different exit on every request. Per its documentation, it understands http, https and socks5 schemes, and treats a missing scheme as http:

package main

import (
	"fmt"

	"github.com/gocolly/colly/v2"
	"github.com/gocolly/colly/v2/proxy"
)

func main() {
	c := colly.NewCollector()

	rp, err := proxy.RoundRobinProxySwitcher(
		"http://203.0.113.7:8080",
		"http://203.0.113.24:3128",
		"socks5://198.51.100.14:1080",
	)
	if err != nil {
		panic(err)
	}
	c.SetProxyFunc(rp)

	c.OnResponse(func(r *colly.Response) {
		fmt.Println(string(r.Body)) // prints the exit IP that httpbin saw
	})

	c.Visit("https://httpbin.org/ip")
	c.Visit("https://httpbin.org/ip") // a different exit than the first visit
}

For a single fixed proxy, c.SetProxy("http://203.0.113.7:8080") is the shortcut; it wraps a one-entry switcher internally. You do not have to hardcode the list either. Our free proxy API returns a fresh pool you can pull at startup and splice straight into RoundRobinProxySwitcher. 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 is doing for you.

How do I check that the proxy is really being used?

Never assume the proxy is working. Confirm the target sees the proxy's address and not yours before you trust an exit with real traffic. The clean check compares a direct request against a proxied one:

func exitIP(client *http.Client) string {
	resp, err := client.Get("https://httpbin.org/ip")
	if err != nil {
		return "error: " + err.Error()
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)
	return string(body)
}

// direct := &http.Client{Timeout: 10 * time.Second}
// proxied := &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)}}
// fmt.Println("real: ", exitIP(direct))
// fmt.Println("proxy:", exitIP(proxied))
Our own terminal with go version 1.24.2, the source of a 34-line program that builds a direct client and a proxied client with http.ProxyURL, and the run: httpbin reports the address of the public proxy, and the direct comparison prints false.
Captured on our own machine on 2 September 2026. The proxy is a public entry from our free proxy list; the program prints only what httpbin saw through it and whether that equals the direct answer, so our own address never appears.

If the two responses show the same IP, the request never went through the proxy. It is also worth checking https://httpbin.org/headers through the proxy, 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.

The two errors a broken proxy gives you

They mean different things, and the wording tells you which:

Get "https://example.com": Forbidden
Get "http://example.com": proxyconnect tcp: dial tcp 203.0.113.7:8080: ...

The first came from a proxy that answered and then refused to open the tunnel: the address is right, the proxy is alive, and it declined this request. The second came from an address with nothing listening: Go never got a proxy at all, and everything after proxyconnect tcp is the dial failure underneath. The exact tail of that second line is the operating system speaking, so it reads differently on Windows, Linux and macOS.

A third case looks like neither. If the request succeeds and the target still sees your own address, the proxy was never used: check that the Transport has a Proxy field at all, and that the variable matches the scheme of the request.

Where to go from here

Two habits keep Go proxy code alive unattended: set http.Client.Timeout so a silent proxy cannot hang a goroutine forever, 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, proxies for web scraping covers choosing the right proxy type and the request hygiene no HTTP client can add for you, the cURL guide is the fastest way to sanity-check a proxy from the shell before you wire it into code, and if you hit an auth wall, how to fix 407 Proxy Authentication Required is the focused fix. When a project graduates to production, get IPs nobody else is burning on our paid pools. For teams that would rather order and rotate plans from a build step, the REST API docs walk through authentication, quoting, ordering and delivery.

Sources

  • net/http package documentation: Transport.Proxy, http.ProxyURL, and http.ProxyFromEnvironment, including the HTTP_PROXY/HTTPS_PROXY/NO_PROXY and localhost rules. The schemes quoted above are from src/net/http/transport.go in Go 1.24.2.
  • golang.org/x/net/http/httpproxy: the package behind ProxyFromEnvironment, and the only place the NO_PROXY matching rules are written out.
  • golang/go issue 18508, 4 January 2017: proxy environment variables did not understand socks5://, which is why older guides route around net/http.
  • Our own measurements, 16 September 2026, Go 1.24.2: nineteen cases against stub HTTP and SOCKS5 proxies on this machine, covering the request shapes, the credential headers, the environment table above and the two error texts. Every server in the run was local, so nothing was sent to anyone else.
  • golang.org/x/net/proxy: SOCKS5, FromURL, the Auth struct, and the Dialer/ContextDialer interfaces used with Transport.DialContext.
  • Colly proxy package: RoundRobinProxySwitcher and the http/https/socks5 schemes it accepts.
  • Go Modules Reference: the module proxy, which is what GOPROXY and proxy.golang.org refer to.

Frequently asked questions

How do I set a proxy for a single request in Go instead of the whole client?
You cannot, and that is by design. net/http attaches the proxy to the Transport, not to an individual http.Request, so the smallest unit you can proxy is an http.Client. The clean pattern is to build one client with a proxied Transport and another without, then call whichever you need. There is no per-request proxy field to set.
Why did my proxy environment variable stop working after I made a custom Transport?
Because http.DefaultTransport sets Proxy: http.ProxyFromEnvironment for you, but a Transport you construct yourself starts with an empty Proxy field and ignores HTTP_PROXY, HTTPS_PROXY and NO_PROXY entirely. Set Proxy: http.ProxyFromEnvironment on your custom Transport to get that behavior back. This trips up almost everyone who first customizes a Transport for TLS or timeout settings.
Does Go's standard library support SOCKS5 proxies?
Yes, in current Go. The documentation of net/http in Go 1.24.2 says Transport.Proxy supports http, https, socks5 and socks5h, and treats socks5 the same as socks5h. We measured it on 16 September 2026 against a stub SOCKS5 proxy: the request went through with nothing but the standard library, and the proxy received the host name rather than an address, so the lookup happened at the proxy. Older guides send you to golang.org/x/net/proxy because this was genuinely missing once.
How do I add a username and password to a Go proxy?
Put them in the proxy URL, http://user:pass@host:port or socks5://user:pass@host:port, and the standard library does the rest. In our own run the HTTP proxy received a Proxy-Authorization Basic header on the plain request and on the CONNECT request, and the SOCKS5 proxy was offered the user name and password method and completed the exchange. A 407 Proxy Authentication Required response means the proxy is alive and only the credentials are wrong.
What does NO_PROXY actually match in Go?
A bare domain matches that domain and every subdomain, and a leading dot matches subdomains only. In our own run NO_PROXY=example.com kept both example.com and api.example.com off the proxy, while NO_PROXY=.example.com kept api.example.com off it and still proxied example.com. A single asterisk turns proxying off completely, and a target on localhost or a loopback address is never proxied whatever you set.
How do I rotate proxies in Colly?
Use proxy.RoundRobinProxySwitcher with your list of proxy URLs and attach it with c.SetProxyFunc. It hands out a different proxy on each request in round-robin order. It understands http, https and socks5 URLs, and an empty scheme is treated as http. For a single fixed proxy, c.SetProxy(url) is the shortcut.

Proxies that don't die mid-job

Residential, ISP, datacenter and mobile, verified by the same engine that runs tens of millions of checks. They read as a real device and hold up under load. Pay as you go, and your balance never expires. $0.44/GB is the 2,000 GB+ rate; a single gigabyte is $0.50/GB, with no minimum order.

129M+ proxy checks run · 100+ countries · HTTP / HTTPS / SOCKS · re-checked every few minutes · no signup

HProxy.

Honest guides and comparisons on proxies, scraping and staying unblocked, from the team that runs the network.

RSS feed