---
title: "HTTP_PROXY, HTTPS_PROXY and NO_PROXY explained"
url: https://proxy.wiki/guides/proxy-environment-variables/
type: Guide
author: "proxy.wiki editorial"
published: 2026-08-29
updated: 2026-08-29
site: proxy.wiki
topics: ["Proxy fundamentals"]
license: CC BY 4.0 — quote freely with attribution to https://proxy.wiki/
---

# HTTP_PROXY, HTTPS_PROXY and NO_PROXY explained

> The four proxy environment variables, tested across curl, wget, Python, Go and Node, including the case rule that silently ignores HTTP_PROXY.

## Key takeaways

- Always set the lowercase http_proxy: curl and wget deliberately ignore the uppercase HTTP_PROXY.
- That exception exists because a CGI request header arrives as HTTP_PROXY, which let visitors redirect outbound traffic.
- no_proxy matching is not portable. Go still proxies the bare apex when you write a leading dot; curl and Python do not.
- curl and Go match CIDR ranges in no_proxy. Python compares strings, so 10.0.0.0/8 never matches an address.
- Node's built-in fetch ignores the variables entirely, so traffic you believe is proxied leaves from your own address.

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](/glossary/http-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](/glossary/proxy-authentication/) 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](/glossary/rotating-proxy/) the reported address describes one [exit node](/glossary/exit-node/) at one moment, not the [pool](/glossary/proxy-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](/glossary/bandwidth-billing/) that is billable traffic you did not intend to send.

- **They cannot express per-request routing.** Work that needs a different [session](/glossary/sticky-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.

## Frequently asked questions

### Why does curl ignore HTTP_PROXY but honour http_proxy?

Because in a CGI environment every inbound request header is exposed as an environment variable with an HTTP_ prefix, so a visitor sending a Proxy header creates HTTP_PROXY. Honouring it would let any visitor redirect the server's outbound traffic. This was disclosed in 2016 as httpoxy, and curl now trusts only the lowercase spelling of that one name. The other variables have no such collision and both cases work.

### Should I set both the uppercase and lowercase versions?

Set the lowercase spelling first, because every client tested honours it. Setting the uppercase versions of https_proxy and no_proxy as well is harmless and helps tools that only check that case. The one you should not rely on alone is uppercase HTTP_PROXY.

### Why is my no_proxy entry ignored in one language and not another?

The variable has no specification, so each client implemented its own matching. Tested on curl 8.7.1, CPython 3.9 and Go 1.24, a leading dot such as .example.org exempts the apex domain in curl and Python but not in Go, and a CIDR range works in curl and Go but not in Python. Write both the apex and the dotted form, and list addresses rather than ranges when Python is involved.

### I set https_proxy in Node and requests still go out directly. Why?

Node's global fetch does not read proxy environment variables. This was confirmed on Node 24.2.0: a request with HTTPS_PROXY set reached the destination unproxied. Node needs an explicit dispatcher, usually ProxyAgent from undici, or a client such as axios or got configured with an agent.

### Why does my systemd service ignore the proxy I exported?

A service does not inherit your login shell. systemd supplies the unit's environment itself, so the setting belongs in the unit file as an Environment= line or an EnvironmentFile, not in .bashrc. Cron behaves the same way, running with a deliberately minimal environment.

## Sources

1. [curl manual page: environment variables and proxy options](https://curl.se/docs/manpage.html)
2. [httpoxy: the CGI namespace collision behind the HTTP_PROXY exception](https://httpoxy.org/)
3. [Python urllib.request: getproxies and proxy_bypass](https://docs.python.org/3/library/urllib.request.html)
4. [Go httpproxy: NO_PROXY matching, including CIDR](https://pkg.go.dev/golang.org/x/net/http/httpproxy)
