LangChain makes two very different kinds of outbound request, and only one of them is worth putting a proxy on. Every chain and agent calls a model endpoint, and most of them also collect data from the open web through document loaders, retrievers and scraping tools. The model call carries your API key, so its rate limit follows the key rather than your address, and no proxy changes that. The data-collection calls hit ordinary websites the same way a scraper does, which means they draw the same rate limits, geo-gates and bot walls, and that is the traffic a proxy is for. Route your loaders and tools through a proxy by configuring the HTTP client each one uses, and leave the model call alone unless a firewall forces your hand.
We run a proxy network and a live proxy checker, so the failure we see most in LangChain code is a pipeline that proxies the wrong half, or one loader that quietly keeps using the real IP because a different loader in the same script speaks a different HTTP library. Every example below is runnable. Where one needs a live proxy, pull a fresh one from our free proxy API, which returns real, recently checked endpoints with no key.
How do you use a proxy with LangChain?
Set the proxy on the loader or tool that fetches the page, not on the model. For the common WebBaseLoader and the loaders built on the requests library, pass a proxies dictionary keyed by scheme, exactly the shape requests expects, and add a timeout so a dead proxy cannot hang the chain.
from langchain_community.document_loaders import WebBaseLoader
loader = WebBaseLoader(
"https://example.com/article",
proxies={
"http": "http://user:pass@203.0.113.7:8080",
"https": "http://user:pass@203.0.113.7:8080",
},
requests_kwargs={"timeout": 15},
)
docs = loader.load()
The key in that dictionary is the scheme of the URL you are fetching, not of the proxy, and both usually point at the same proxy. Set only the http key and every https:// fetch skips the proxy and goes out on your real IP, which is the same silent no-op we cover in the Python requests guide.
The two sides of LangChain traffic
This is the distinction that decides where the proxy belongs, and getting it backwards is the most common mistake. A chain or agent talks to a model endpoint, and it reaches out to the web to gather context. Those two flows have opposite economics.
The model call is authenticated. Whether you route it through ten IPs or none, the provider counts every token against your key, so a proxy cannot lift a rate limit that is enforced per account. That boundary is worth stating plainly, because a lot of teams reach for proxies to fix a 429 from their model provider and it never works. We wrote up exactly why in LLM API rate limits and proxies.
The data-collection call is not authenticated by you. When a loader pulls a product page or a retriever crawls a set of sources, the target sees an IP and applies its own defenses to that IP: rate limits, country gating, and the bot walls that challenge automation. Spread that traffic across many IPs and the whole picture changes. This is the same shape of problem covered in proxies for AI agents, because a LangChain agent that browses is an AI agent by another name.
Model API call
carries your key, quota follows the key
Loaders and tools
hit the open web, metered per IP
Proxy the data side
residential for defended sites
Leave the key call
an IP change does not raise its limit
Proxying WebBaseLoader and the document loaders
WebBaseLoader is the loader most chains start with, and it takes the proxies dictionary directly, as above. For a whole set of loaders that are built on requests, the blunt instrument is the environment, which requests reads by default:
import os
os.environ["HTTP_PROXY"] = "http://user:pass@203.0.113.7:8080"
os.environ["HTTPS_PROXY"] = "http://user:pass@203.0.113.7:8080"
# every requests-based loader created after this line now routes through the proxy
One caveat carries real weight: LangChain loaders do not all use the same HTTP client, so a proxy set one way does not automatically cover another. The synchronous WebBaseLoader.load() uses requests, its aload() fan-out uses aiohttp, some of the newer loaders use httpx, and the Playwright loaders drive a real browser. When a loader ignores your proxies dict, the reason is almost always that it speaks a different library, and the proxy has to go where that library takes it. The mechanics for each live in the aiohttp guide and the httpx guide, and for a browser-driven loader the proxy goes on the browser launch, the same way it does in the Playwright guide.
Rotating a pool for scraping tools and retrievers
A single IP pulling every source is the exact pattern rate limiters watch for, and a retriever that fans out across dozens of pages hits that wall fast. Spread the load across a pool and pick a fresh exit per attempt, retrying on a different proxy when one fails:
import random
import requests
from langchain_community.document_loaders import WebBaseLoader
POOL = requests.get(
"https://hproxy.com/api/proxy-list",
params={"format": "txt", "protocol": "http", "recent": "true", "limit": 50},
timeout=15,
).text.split()
def load_url(url, tries=4):
for _ in range(tries):
proxy = f"http://{random.choice(POOL)}"
try:
loader = WebBaseLoader(
url,
proxies={"http": proxy, "https": proxy},
requests_kwargs={"timeout": 15},
)
return loader.load()
except Exception:
continue # dead or blocked exit, try the next one
raise RuntimeError(f"all {tries} proxies failed for {url}")
The load-bearing part is retrying with a different proxy on failure, because on any pool some exits are always down. The same loop drops into a custom LangChain tool: wrap the fetch in a @tool function so the agent calls a rotating scraper instead of a bare HTTP client, and it inherits the retry behavior for free. When a hand-rolled pool outgrows a list, a rotating gateway that hands you one endpoint and a fresh residential IP per request removes the list management entirely.
Putting a proxy on the model call, when you actually need to
There is one legitimate reason to proxy the model call, and it is never rate limits. If your code runs behind a corporate firewall that forces all outbound traffic through an egress proxy, or the provider is reachable only from a specific region, you route the model client through a proxy to satisfy that network, not to change any quota. In langchain-openai you pass an httpx client:
import httpx
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="gpt-4o-mini",
http_client=httpx.Client(proxy="http://user:pass@203.0.113.7:8080"),
http_async_client=httpx.AsyncClient(proxy="http://user:pass@203.0.113.7:8080"),
)
Pass both http_client and http_async_client so the synchronous and streaming paths agree, and note that httpx 0.26 and later use the singular proxy= argument where older versions used proxies=. This is a network-plumbing move, and it leaves the provider's per-key limit exactly where it was.
Where to go from here
Two habits keep a LangChain data pipeline alive once it leaves the demo: proxy the collection side and not the model, and pull from a pool that is actually alive. For the second, our free proxy API returns recently checked endpoints with no key, ideal for testing every loader in this guide before you wire in a paid pool, and the proxy checker confirms an exit is live and leaving from the country you expect before you trust it in a chain.
From here, the requests guide is the reference for the proxies dict that most loaders inherit, proxies for AI agents covers the rotation decision when a task holds state across many steps, and proxies for LLM scraping is the wider picture for building datasets from the web. When the friendly fetches should stay cheap and only the defended sites need to look residential, our pay-as-you-go residential proxies at $0.44 per GB pick up where a free pool gives out.
Sources
- LangChain: WebBaseLoader reference: the
proxiesandrequests_kwargsconstructor arguments. - httpx: Proxies: the
proxy=client argument and the version change fromproxies=. - langchain-openai: ChatOpenAI: the
http_clientandhttp_async_clientfields used to route the model call.