← all guides

Proxy env vars that route more than you meant

Every scraping setup eventually hits the same moment: someone exports http_proxy in a shell to get one script working, it works, and the variable just stays there. Weeks later, bandwidth on a residential plan is disappearing faster than the scrape volume explains, or a CI job starts timing out against an internal service for no obvious reason. Nine times out of ten, the cause is the same thing that made the original fix easy: environment variables don’t know what you meant them for. They apply to everything that inherits them.

This isn’t a scraping-specific quirk. It’s how process environments have worked since before HTTP existed. But when you’re running proxy infrastructure at any real scale, the blast radius of a loosely scoped http_proxy gets a lot bigger than it does on a laptop.

The variables and what each one actually governs

The convention (never a formal standard, which is part of the problem) is four variables: http_proxy, https_proxy, all_proxy, and no_proxy. http_proxy covers plain HTTP requests. https_proxy covers HTTPS, which matters because most clients tunnel HTTPS through a proxy via a CONNECT request rather than routing it the way they route plain HTTP, so a tool can very much honor one and ignore the other. all_proxy is a catch-all some tools respect and many don’t. no_proxy is a comma-separated list of hosts that should bypass the proxy entirely, and it’s the one people forget until something breaks.

None of this is guaranteed behavior. It’s convention that grew out of early Unix tools like wget and curl, and other tools followed along inconsistently. Python’s requests and urllib3 honor these variables by default. Node’s built-in fetch (undici) historically does not, and needs an explicit proxy agent wired in. Headless browsers like Playwright and Puppeteer ignore them completely unless you pass a proxy flag at launch. If your pipeline mixes a Python scraper, a Node build step, and a headless browser for JS-rendered pages, setting http_proxy once at the container level does not give you consistent proxy coverage across all three. You have to check what each tool actually does, not assume the env var is universal plumbing.

Case sensitivity is not cosmetic

On Linux and macOS, environment variables are case-sensitive, which means http_proxy and HTTP_PROXY are two entirely separate variables that can coexist and hold different values. On Windows, the OS treats environment variable names as case-insensitive, so there is no distinction: setting HTTP_PROXY and http_proxy in the same PowerShell session is setting the same variable twice.

That difference matters when a scraping pipeline crosses platforms, which most do at some point between a developer’s machine and a Linux container in production. A script that reads os.environ.get("HTTP_PROXY") on a Linux box will silently miss a value that was only ever set as http_proxy, and nothing errors. It just quietly doesn’t proxy the request. The failure mode isn’t a crash, it’s traffic going out unproxied, which for a scraper means requests leaving from the origin IP instead of the pool you thought you’d configured.

Environment variables are inherited, not scoped

The part that actually causes the surprising bandwidth bills: a variable exported in a shell is inherited by every child process spawned from that shell, indefinitely, until the shell exits or the variable is explicitly unset. It doesn’t matter whether the child process has anything to do with scraping.

Say you export http_proxy in a shell to route a target script through a paid mobile exit, because that’s the IP class the target site expects for that traffic. If, in that same shell session, you then run pip install, git pull, or a curl health check against your own monitoring endpoint, all of those inherit the identical proxy setting. Package downloads and internal health checks start consuming the same metered, rotating, per-request-billed proxy pool as the actual scrape target. On a mobile plan billed by data volume or session count, that’s real cost for zero scraping value. On a provider that limits sessions per IP or flags unusual destination patterns from a single exit, routing internal traffic through it can also trip rate limits that have nothing to do with the site you’re actually trying to reach cleanly.

Containers make this worse in one direction and better in another. Worse, because ENV http_proxy=... in a Dockerfile or a container’s environment config applies to every process the container ever runs, including package managers during build and any sidecar tooling at runtime, with no natural boundary. Better, because a container at least gives you a clean unit to scope the variable to, rather than a long-lived interactive shell that accumulates exports over months.

The httpoxy problem, as a cautionary example

In 2016, a set of CGI-based server environments (PHP, some Python and Go setups running under CGI or FastCGI) were found to have a specific problem: the CGI specification maps every incoming HTTP request header to an environment variable prefixed with HTTP_. A client-supplied header literally named Proxy gets mapped to the environment variable HTTP_PROXY. If the server application then made its own outbound HTTP requests using a library that honored HTTP_PROXY from the environment, as many did by default, an external client could set an arbitrary Proxy header on its request and cause the server’s own outbound calls to route through a host of the client’s choosing. That’s CVE-2016-5385, publicly disclosed and well documented, and it’s worth understanding as an operator even though it’s a server-side issue rather than a scraping technique.

The defensive fixes that came out of it are the relevant part here. Reverse proxies and web servers were updated to strip an incoming Proxy header before it reaches the application, since it has no legitimate use as a client-supplied header. HTTP client libraries were patched to stop trusting HTTP_PROXY unconditionally in server contexts. The lesson generalizes past that one CVE: any time a proxy variable’s value can be influenced by something outside your own control, whether that’s an inherited shell export, a CI environment leaking between jobs, or a request header mapped into env, you’ve created a path where traffic goes somewhere you didn’t decide it should go. Treat proxy env vars as configuration that needs a trust boundary, not as a convenience default.

What loose scoping actually costs on a proxy farm

Running proxy infrastructure for real makes the abstract risk concrete. A rotating residential or mobile pool is priced and rate-limited around expected scraper behavior: a session, a request cadence, a data volume tied to actual target pages. When unrelated processes on the same host share that pool because a proxy variable leaked scope, three things tend to happen. Bandwidth gets consumed by traffic that was never meant to justify the cost of a residential or mobile exit, like a package install that could have gone out on the origin connection with zero benefit from the proxy. Session or rotation limits get consumed faster, which on providers that meter rotations or concurrent sessions per account shortens the effective runway for the scrape that actually matters. And diagnosing it is genuinely annoying, because the symptom (a spike in proxy usage, or a target site suddenly behaving oddly) doesn’t obviously point back to an unrelated curl health check three services away that happened to inherit the same shell environment.

Keeping proxy scope tight

The fix isn’t complicated, it’s just a habit. Set proxy configuration at the narrowest point that needs it: pass proxies explicitly to the HTTP client (a requests.Session with a proxies dict, a per-launch flag for a headless browser) rather than exporting a shell-wide variable and hoping every tool picks it up correctly. If a shell-level or container-level variable is genuinely necessary, populate no_proxy deliberately with internal hosts, localhost, and any cloud metadata endpoints, and don’t treat that list as optional boilerplate. And check what each tool in a pipeline actually does with these variables rather than assuming consistency, since the gap between “reads it” and “ignores it” is exactly where requests end up going out unproxied without any error to flag it.

None of this makes a proxy setup undetectable or guarantees clean outcomes against any given target. It just keeps the proxy pool doing the job it’s actually paid to do, instead of quietly absorbing traffic nobody meant to send through it.

If you’re weighing residential, mobile, or datacenter proxies for a scraping setup and want comparisons grounded in how the infrastructure actually behaves, head to the Proxy Scraping homepage.

Get new guides and videos first — join the Telegram channel.

proxies
Need proxies that survive the block wall?

Singapore Mobile Proxy runs real 4G/5G mobile IPs on rotating SIMs — the carrier-grade addresses most of these targets still trust.

see plans →
read on
More scraping guides

The rest of the field manual: target-site playbooks, library walkthroughs, provider reviews, and anti-bot troubleshooting.

browse all guides →