Skip to content

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 TeamJuly 17, 2026Updated October 22, 202613 min read

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

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, and an http.Request has no proxy field of its own. SOCKS5 goes in the same field: current Go takes a socks5:// URL there, with no extra package.

Older guides get that last sentence wrong, so everything below was measured rather than repeated. The runs used Go 1.24.2 on September 16, 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.

The two meanings meet in search. Keyword Planner puts "goproxy", the name of the module setting and also of a Go proxy library, at 2,900 searches a month worldwide. "golang http proxy" gets 210 and "golang socks5 proxy" 170.

Average monthly searches for Go and proxy, worldwide, October 2025 to September 2026
goproxythe module setting, and a library
2,900
golang proxyeither meaning
1,600
golang reverse proxybuilding a proxy
320
golang http proxythis page
210
golang socks5 proxythis page
170
proxy.golang.orgthe module proxy
70
golang http client proxythis page
40
goproxy alone drew more searches than every phrase about proxying Go's own requests together.
Source: Google Keyword Planner, worldwide 12-month averages, read October 22, 2026 · hproxy.comProxy.

Go's standard library has clean proxy support built in. The catches lie elsewhere. The proxy lives on the http.Transport rather than on a request, and a custom Transport silently drops the environment handling you got for free. The environment is also read only once per process. This guide covers the standard-library way, SOCKS5 without an extra package and authentication. It then covers what your proxy variables really match, rotating a pool with Colly, and the check that proves the proxy carries your traffic.

Every example is a complete, runnable program. Where one needs a live proxy, pull a fresh one from our free proxy API, which returns recently checked entries 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, and every request from that client inherits it.

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 · hproxy.comProxy.

What Go's proxy support does not do

Five 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 without checking it. 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. A plain HTTP proxy thus carries your encrypted traffic without seeing inside it. The user:pass in the proxy URL does real work too. 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. It 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:

What http.ProxyFromEnvironment answered for each environment, Go 1.24.2

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

HProxy, our own run of Go 1.24.2, September 16, 2026: one fresh process per environment, no request sent

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 and leaves y.com alone. 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. That 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 asked before the variable was set and again after setting it. 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 September 16, 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 behavior 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 January 4, 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. We did not run it for this page: the code above follows its documentation, and 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 gives you one endpoint and a fresh residential IP per request, which removes list management entirely. Rotating a real list first is still the best way to understand what that gateway does 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))
Captured on our own machine on September 2, 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. Check https://httpbin.org/headers through the proxy as well. A transparent proxy can forward your real address in X-Forwarded-For even while origin looks changed. If you would rather not script it, our proxy checker runs that whole battery in one paste. It reports the exit IP, the anonymity grade, the real location and the latency.

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 and the proxy is alive, but 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 shows when each entry was last checked. The free API hands the entries 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 check a proxy from the shell before you wire it into code. If you hit an auth wall, how to fix 407 Proxy Authentication Required is the focused fix. When a project goes to production, our paid pools charge $0.50/GB for one GB of residential traffic, falling to $0.44/GB at 2,000 GB+. 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, January 4, 2017: proxy environment variables did not understand socks5://, which is why older guides route around net/http.
  • Our own measurements, September 16, 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.
  • Google Keyword Planner, worldwide 12-month averages for the searches in the chart, read October 22, 2026.

Frequently asked questions

How do I set a proxy for a single request in Go instead of the whole client?

Not on the request itself: net/http keeps the proxy on the Transport, and an http.Request has no proxy field. The Transport's Proxy function is called for every request, though, so it can choose a proxy per request, for example from a value in the request's context. The simpler pattern is two clients, one with a proxied Transport and one without.

Why did my proxy environment variable stop working after I made a custom Transport?

Because http.DefaultTransport sets Proxy: http.ProxyFromEnvironment for you. 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. It catches many people the first time they customize a Transport for TLS or timeouts.

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 September 16, 2026, against a stub SOCKS5 proxy. The request went through with nothing but the standard library. 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. 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. NO_PROXY=.example.com kept api.example.com off it and still proxied example.com. A single asterisk turns proxying off completely. 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