Setting http_proxy looks like the simplest way to put a whole machine behind a proxy. It is also the configuration that most often behaves differently from how you read it, because the variables are a convention rather than a standard. No RFC defines them. Every client implemented its own reading of the same four names, and the readings disagree.
This guide records what four widely used clients actually do, tested rather than quoted. Where they diverge, the divergence is the whole point: a rule that holds in Python quietly fails in Go, and the failure is silent traffic going somewhere you did not intend.
#The four variables
| Variable | Applies to | Notes |
|---|---|---|
http_proxy |
Requests to http:// URLs |
The one variable with a case rule; see below |
https_proxy |
Requests to https:// URLs |
Sets the proxy used for the CONNECT tunnel |
all_proxy |
Any scheme with no more specific variable | Not universal; curl honours it, Go does not |
no_proxy |
Exemptions from all of the above | Comma-separated; matching rules differ per client |
The value takes the same form as the -x argument to curl: scheme://host:port, with optional inline credentials. A username and password placed here is visible to every process that can read the environment, which is a reason to prefer per-client configuration for anything sensitive.
# The conventional form
export http_proxy="http://gateway.example:8000"
export https_proxy="http://gateway.example:8000"
export no_proxy="localhost,127.0.0.1,.internal.example"
#Case matters, and not in the direction you expect
The instinct is that the uppercase and lowercase spellings are interchangeable. For three of the four variables they usually are. For http_proxy they are not, and the exception exists for a security reason.
In a CGI environment, every inbound HTTP request header is exposed to the script as an environment variable with an HTTP_ prefix. A request carrying a Proxy: header therefore arrives as HTTP_PROXY. If the HTTP client inside that script honoured it, any visitor could redirect the server’s outbound traffic to a host of their choosing. This was disclosed in 2016 as httpoxy, and the fix in most clients was to stop trusting the uppercase spelling of that one name.
The result is that the safest-looking spelling is the one most likely to be ignored. Tested on curl 8.7.1, GNU wget, CPython 3.9 and Go 1.24, each against a proxy address that refuses connections, so that “the request succeeded” proves the proxy was not used:
| Client | http_proxy (lowercase) |
HTTP_PROXY (uppercase) |
|---|---|---|
| curl 8.7.1 | Honoured | Ignored |
| GNU wget | Honoured | Ignored |
Python 3.9 urllib / requests |
Honoured | Honoured, unless REQUEST_METHOD is set |
Go 1.24 net/http |
Honoured | Honoured |
The Python row is the subtle one. CPython treats the presence of REQUEST_METHOD in the environment as evidence that it is running under CGI, and drops uppercase HTTP_PROXY only in that case. The same script behaves differently depending on a variable that has nothing to do with proxying:
$ HTTP_PROXY=http://127.0.0.1:9 python3 -c \
"import urllib.request as u; print(u.getproxies_environment())"
{'http': 'http://127.0.0.1:9'}
$ REQUEST_METHOD=GET HTTP_PROXY=http://127.0.0.1:9 python3 -c \
"import urllib.request as u; print(u.getproxies_environment())"
{}
The practical rule: always set the lowercase spelling. Every client tested honours it, and it is the only spelling with no special case attached. Setting both costs nothing and removes the question entirely.
#no_proxy: four clients, four sets of rules
The exemption list is where the real divergence lives. The same no_proxy string produces different routing in different runtimes, and nothing warns you.
Each row below was tested against a proxy address that refuses connections. “Bypass” means the request reached the destination directly. “Proxied” means the client tried the proxy, so the exemption did not apply. A control with an empty no_proxy confirmed the test detects proxy use.
no_proxy value |
Requested host | curl 8.7.1 | Python 3.9 | Go 1.24 |
|---|---|---|---|---|
.example.org |
api.example.org |
Bypass | Bypass | Bypass |
.example.org |
example.org |
Bypass | Bypass | Proxied |
example.org |
api.example.org |
Bypass | Bypass | Bypass |
10.0.0.0/8 |
10.1.2.3 |
Bypass | Proxied | Bypass |
* |
Any | Bypass | Bypass | Bypass |
Two rows deserve attention.
The leading dot. Go reads .example.org as “subdomains of example.org, and not the apex itself”. curl and Python read it as “example.org and anything below it”. A team that writes no_proxy=".internal.corp" and expects internal.corp to be exempt gets that behaviour in Python and the opposite in Go. Write the apex explicitly — internal.corp,.internal.corp — and the ambiguity disappears.
CIDR ranges. curl and Go both match an address against a network in no_proxy. CPython’s urllib does not; it compares strings, so 10.0.0.0/8 never matches 10.1.2.3 and the request goes to the proxy. If a Python service must reach an internal network directly, list the addresses or hostnames rather than the range.
#Which clients read the environment at all
An assumption worth checking before you debug anything else: not every client looks at these variables. Setting them and seeing no change usually means the client never read them, not that the value is wrong.
| Client | Reads proxy environment by default |
|---|---|
| curl, wget | Yes |
Python requests, urllib |
Yes; disable with session.trust_env = False |
Go net/http default transport |
Yes |
Node.js global fetch (tested on 24.2.0) |
No |
| Browsers | No; they use system or PAC configuration |
Node’s built-in fetch is the common surprise. On Node 24.2.0 a request made with HTTPS_PROXY set reached the destination directly. Node needs an explicit dispatcher, typically ProxyAgent from undici, or an agent-aware client such as axios or got. Setting the variable and assuming it applied is how traffic you believed was proxied leaves from your own address.
#Proving which variable actually applied
Do not infer this. Ask the destination to report the address it saw, then compare against the same request with the environment cleared:
# With whatever the environment says
curl -s https://api.ipify.org
# With every proxy variable removed, as a control
env -u http_proxy -u https_proxy -u HTTP_PROXY -u HTTPS_PROXY \
-u all_proxy -u ALL_PROXY -u no_proxy -u NO_PROXY \
curl -s https://api.ipify.org
If the two agree, no proxy was applied, whatever the variables say. curl will also tell you directly:
curl -v https://example.com/ 2>&1 | grep -i 'proxy\|Connected to'
A line reading Connected to gateway.example rather than the destination host confirms the tunnel. On a rotating endpoint the reported address describes one exit node at one moment, not the pool as a whole.
#The three places the variables do not reach
Most reports of “I set the proxy and nothing happened” trace to one of three boundaries the environment does not cross.
A container does not inherit the host shell. docker run starts a process with its own environment. Variables exported in your terminal are not passed unless you say so, and a build is a separate case again, because docker build runs each step in its own container:
# Runtime
docker run -e http_proxy -e https_proxy -e no_proxy myimage
# Build time, and only for ARGs the Dockerfile declares
docker build --build-arg http_proxy --build-arg https_proxy .
Pulling the image itself is a third case. That is the Docker daemon’s own outbound request, not your shell’s, so it reads the daemon configuration rather than anything you exported.
A service manager does not inherit your login shell. A unit started by systemd gets the environment systemd gives it. Exporting a variable in .bashrc and restarting the service changes nothing; the setting belongs in the unit, usually as an Environment= line or an EnvironmentFile. The same applies to anything started by cron, which runs with a deliberately minimal environment.
Windows uses a different mechanism entirely. The lowercase convention comes from Unix. Native Windows HTTP stacks read the WinHTTP or WinINET configuration instead, so a tool built on those ignores the variables even when they are set. Cross-platform tools that carry their own HTTP client — curl, Python, Go — still read them, and PowerShell sets them with a different syntax:
$env:http_proxy = "http://gateway.example:8000"
$env:https_proxy = "http://gateway.example:8000"
The pattern behind all three: the variables configure a process, and they only apply to processes that inherited them from a parent that had them. Whenever the request is made by something you did not start from that shell, they do not apply.
#When environment variables are the wrong tool
They are process-wide and inherited. That is convenient for a one-off shell session and a liability everywhere else.
- Credentials leak into process listings and logs. Anything that dumps the environment on error — a crash reporter, a debug endpoint, a CI log — dumps the password with it.
- Everything inherits them. A package manager, a telemetry agent and a health check all start using the proxy, and on metered bandwidth that is billable traffic you did not intend to send.
- They cannot express per-request routing. Work that needs a different session or country per request needs client-level configuration.
Use the environment for interactive debugging and for tools that offer no other configuration. For anything running unattended, configure the client directly, so the setting is visible in the code that depends on it rather than in the shell that happened to start it.
#How this was tested
Every result above came from running the command, not from documentation. Each proxy variable pointed at http://127.0.0.1:9, a port that refuses connections, so a successful request proves the proxy was skipped and a failure proves it was attempted. A control run with the exemption list empty confirmed the method detects proxy use in each client. Results were stable across repeated runs.
Versions: curl 8.7.1, GNU wget, CPython 3.9 with requests 2.32.3, Go 1.24.4, Node.js 24.2.0. Behaviour changes between versions, so re-run the check on your own runtime before depending on any row.