HTTP Headers for Web Scraping
Web scraping headers are the HTTP request headers your scraper sends with every request, and the ones that decide whether it looks like a browser are User-Agent, Accept, Accept-Language, Accept-Encoding and Referer, plus the Sec-CH-UA and Sec-Fetch-* headers a real Chrome adds. Send one complete, consistent set copied from the browser you claim to be, because a library default such as python-requests/2.x is the easiest bot signal for a site to spot. Headers fix the client side; for the IP side, ProxyField residential proxies give you 412M+ residential IPs from $1.95/GB, and you can start with the Easy Trial: 1 GB for $2.95.
- residential IPs
- 412M+
- from, traffic never expires
- $1.95/GB
- Easy Trial
- 1 GB for $2.95
On this page 13 sections
- Which headers to send when web scraping
- Copy real browser headers instead of guessing
- Set headers and the user agent in Python requests
- Set the user agent in curl and wget
- Set the user agent in Selenium
- Set the user agent and headers in Puppeteer
- Set headers and the user agent in Playwright
- Set the user agent and headers in Scrapy
- Set the user agent in Postman
- Header order, case and HTTP/2
- Should you rotate user agents?
- Headers and proxies
- HTTP Headers for Web Scraping FAQ
Which headers to send when web scraping
A desktop Chrome sends about a dozen headers on a page load. These are the ones sites look at most, with what a browser sends:
| Header | What it tells the site | Browser-like value |
|---|---|---|
User-Agent | Browser, version and operating system | Mozilla/5.0 ( |
Accept | Content types the client wants | text/ (copy your browser's exact value) |
Accept-Language | Preferred languages | en-US,en;q=0.9; match it to the proxy country |
Accept-Encoding | Compression the client can decode | gzip, deflate, br, zstd; only list what your client can decompress |
Referer | The page that linked here | The listing page for a product page; none for the first visit |
Sec-CH-UA, Sec-CH-UA-Mobile, Sec-CH-UA-Platform | Chrome's client hints: brand, mobile or not, platform | "Google Chrome";v="156", "Chromium";v="156", "Not A(, ?0, "Windows" (example: the "Not A(Brand" entry is a placeholder brand that changes with the Chrome version) |
Sec-Fetch-Site, -Mode, -User, -Dest | How the request was triggered | none, navigate, ?1, document for a typed URL |
Upgrade-Insecure-Requests | The client prefers HTTPS | 1 on page loads |
Cookie | The session the site gave you | Whatever the site set on earlier responses |
The client hints matter because Chromium browsers now send a reduced User-Agent and move the details into Sec-CH-UA-* headers (MDN: User-Agent reduction; MDN: Client hints). A request that claims Chrome 156 in User-Agent but has no Sec-CH-UA is a mismatch a site can test for. Firefox and Safari don't send client hints, so a Firefox User-Agent without them is consistent.
What not to send: headers a browser never sends on a page load, such as X-Requested-With on a navigation, a Content-Type on a GET, or a header with your tool's name.
Copy real browser headers instead of guessing
The easiest consistent set is the one your own browser sends:
- Open the target page in Chrome or Firefox, open DevTools and go to the Network tab.
- Reload, click the first document request, and read Request Headers.
- Right-click it and choose Copy as cURL to get every header in one command.
- Keep
User-Agent,Accept,Accept-Language,Accept-Encoding, theSec-*headers andUpgrade-Insecure-Requests. DropCookieunless you mean to reuse that session, and dropContent-LengthandHost, which your client sets.
Refresh the set when your browser updates; the version in User-Agent and Sec-CH-UA moves every few weeks. Current strings for each browser are in our list of user agents for scraping.
Set headers and the user agent in Python requests
requests sends User-Agent: python-requests/<version> unless you replace it (requests source, default_user_agent). Set the headers on a Session, so cookies and connections carry over:
import requests
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:158.0) Gecko/20100101 Firefox/158.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate",
"Upgrade-Insecure-Requests": "1",
}
proxy = "http://<login>-country-us-city-new_york:<password>@gate.proxyfield.io:7000"
s = requests.Session()
s.headers.update(headers)
r = s.get("https://httpbin.org/headers", proxies={"http": proxy, "https": proxy}, timeout=30)
print(r.json()["headers"]) # what the site receivedThe example claims Firefox and leaves out br and zstd, which plain requests can't decode without extra packages. Even with perfect headers, requests has its own TLS fingerprint, which some anti-bot systems check; that is covered in TLS and TCP fingerprinting. Proxy setup in Python is on Python requests with proxies.
Set the user agent in curl and wget
curl sends curl/VERSION by default; -A replaces it and -H adds any header (curl man page). wget sends Wget/VERSION; --user-agent and --header do the same (GNU Wget manual).
curl -x gate.proxyfield.io:7000 -U "<login>-country-us-city-new_york:<password>" \
-A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36" \
-H "Accept-Language: en-US,en;q=0.9" \
https://httpbin.org/headers
wget --user-agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:158.0) Gecko/20100101 Firefox/158.0" \
--header="Accept-Language: en-US,en;q=0.5" -O - https://httpbin.org/headersMore on each tool: cURL with proxies, wget with proxies.
Set the user agent in Selenium
A real browser already sends browser headers, so in Selenium you usually change only the user agent, and only to match the proxy's location or a device. In Chrome, pass it as a launch argument; other headers go through the Chrome DevTools Protocol:
from selenium import webdriver
opts = webdriver.ChromeOptions()
opts.add_argument("--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36")
driver = webdriver.Chrome(options=opts)
driver.execute_cdp_cmd("Network.enable", {})
driver.execute_cdp_cmd("Network.setExtraHTTPHeaders", {"headers": {"Accept-Language": "de-DE,de;q=0.9"}})
driver.get("https://httpbin.org/headers")
print(driver.find_element("tag name", "body").text)
driver.quit()In Firefox, set the preference general.useragent.override on FirefoxOptions. Keep the version in the string equal to the browser you run; a Chrome 120 string on Chrome 156 is a mismatch. Proxy setup for Selenium: Selenium with proxies.
Set the user agent and headers in Puppeteer
Puppeteer's current form is page.setUserAgent({ userAgent }); the plain string form is marked obsolete in the Puppeteer 25 docs (Puppeteer: Page.setUserAgent). Extra headers go through page.setExtraHTTPHeaders:
import puppeteer from "puppeteer";
const browser = await puppeteer.launch({ args: ["--proxy-server=http://gate.proxyfield.io:7000"] });
const page = await browser.newPage();
await page.authenticate({ username: "<login>-country-us-city-new_york", password: "<password>" });
await page.setUserAgent({ userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36" });
await page.setExtraHTTPHeaders({ "Accept-Language": "en-US,en;q=0.9" });
await page.goto("https://httpbin.org/headers");
console.log(await page.evaluate(() => document.body.innerText));
await browser.close();Headless Chrome's default user agent contains HeadlessChrome, which is why many scrapers set it. More: Puppeteer with proxies.
Set headers and the user agent in Playwright
Playwright sets these per browser context: user_agent, locale (which also sets Accept-Language) and extra_http_headers (Playwright: Browser.new_context):
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(proxy={"server": "http://gate.proxyfield.io:7000",
"username": "<login>-country-us-city-new_york", "password": "<password>"})
context = browser.new_context(
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36",
locale="en-US",
timezone_id="America/New_York",
extra_http_headers={"Referer": "https://example.com/"},
)
page = context.new_page()
page.goto("https://httpbin.org/headers")
print(page.inner_text("body"))
browser.close()Setting locale and timezone_id to the proxy's country keeps the headers and the JavaScript side in agreement. More: Playwright with proxies.
Set the user agent and headers in Scrapy
Scrapy sends Scrapy/VERSION (+https://scrapy.org) unless you change the USER_AGENT setting, and its DEFAULT_REQUEST_HEADERS default to an Accept value and Accept-Language: en (Scrapy settings). Set both in settings.py:
# settings.py
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:158.0) Gecko/20100101 Firefox/158.0"
DEFAULT_REQUEST_HEADERS = {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
}For one request, pass headers={...} to scrapy.Request. Proxies in Scrapy: Scrapy with proxies.
Set the user agent in Postman
Postman sends User-Agent: PostmanRuntime/<version> as an auto-generated header. To change it, add a User-Agent key on the request's Headers tab; your value replaces the generated one. Postman proxy setup: Postman with proxies.
Header order, case and HTTP/2
Browsers send headers in a fixed order, and some anti-bot systems compare the order with the one the claimed browser uses. Most HTTP libraries keep the order of the dictionary you pass but add their own headers first or last, so the result is rarely a browser's order. Over HTTP/2, header names are lowercase and the request starts with pseudo-headers (:method, :authority, :scheme, :path), whose order also differs by client. You can't fix that with headers alone; a real browser, or a client that imitates one at the TLS and HTTP/2 level, sends it right.
Should you rotate user agents?
Rotate only within strings that are real and current, and keep each one for a whole session. A new random user agent on every request from the same IP and cookie looks less like a person, not more. What helps:
- A small set of current strings from the browsers you can imitate fully (the matching
Sec-CH-UAincluded). - One user agent per session or per sticky IP, so the IP, cookies and headers stay together.
- No outdated versions. A Chrome version from two years ago is rarer than a bot.
The fake-useragent Python package, a common source of random strings, was archived in March 2026 (GitHub), so a hand-kept list from your own browsers is the safer source. Why the user agent matters at all is explained in what is a user agent.
Headers and proxies
A proxy changes the IP the site sees; it doesn't fix your headers, and headers don't fix a blocked IP. Two points connect them:
- Match locale headers to the proxy country. A German residential IP with
Accept-Language: en-USis possible but rarer thande-DE. With ProxyField you choose country, region and city per request, so setAccept-Language(or Playwright'slocale) to the same place. - Over HTTPS, the proxy can't touch your headers. Your client opens a tunnel with
CONNECTand the headers travel encrypted inside it. Plain-HTTP proxies may add headers such asViaorX-Forwarded-For, which is one way sites spot proxies (can websites detect proxies).
To check what a site receives, request an echo endpoint such as https://httpbin.org/headers through the same proxy and client.
Headers handle the client; the IP is the other half. ProxyField residential proxies with country, region and city targeting, per-request rotation or sticky sessions up to 36 h, from $1.95/GB with no subscription. Start with the Easy Trial (1 GB); traffic never expires, and all purchases are final.
HTTP Headers for Web Scraping FAQ
What are HTTP headers in web scraping?
ProxyField's 412M+ residential IPs cover the IP side, and HTTP headers cover the client side: they are the 10 or so name-value lines your scraper sends with each request, such as User-Agent, Accept, Accept-Language, Accept-Encoding and Referer, that tell the site which client is asking and what it accepts. Sites compare them with what a real browser sends, so a missing or default header is a common reason scrapers get blocked.
Which headers should I send when web scraping?
Send the set your own browser sends on a page load: User-Agent, Accept, Accept-Language, Accept-Encoding, Upgrade-Insecure-Requests and, for Chrome, the Sec-CH-UA and Sec-Fetch headers. Copy them from DevTools with "Copy as cURL" and keep them consistent with the browser and version you claim.
What is the python-requests default user agent?
It is python-requests/ followed by the library version, such as python-requests/2.32.x. Many sites block that string outright, so set your own User-Agent in the headers of a requests.Session.
How do I set a user agent in Python requests?
Pass it in the headers: requests.get(url, headers={"User-Agent": "..."}), or call session.headers.update({...}) once so every request in the session carries it. Add the matching Accept and Accept-Language headers too.
How do I set a user agent in curl?
Use -A "your user agent" or -H "User-Agent: your user agent"; without it curl sends curl/VERSION. An empty -A "" removes the header entirely.
How do I set a user agent in wget?
Use --user-agent="your user agent"; wget otherwise sends Wget/VERSION. Add other headers with --header="Name: value".
How do I set a user agent in Selenium?
In Chrome, add --user-agent=... to ChromeOptions before you start the driver; in Firefox, set the general.useragent.override preference. Other headers in Chrome go through the DevTools Protocol command Network.setExtraHTTPHeaders.
How do I set a user agent in Puppeteer?
Call await page.setUserAgent({ userAgent: "..." }) before page.goto(), and page.setExtraHTTPHeaders({...}) for other headers. The default headless user agent contains HeadlessChrome, which is why many scrapers change it.
How do I set headers in Playwright?
Set them on the browser context: browser.new_context(user_agent="...", locale="de-DE", extra_http_headers={...}). The locale option sets Accept-Language and the language the page's JavaScript sees, so the two agree.
How do I set a user agent in Scrapy?
Set USER_AGENT in settings.py; the default is Scrapy/VERSION (+https://scrapy.org). Other default headers go in DEFAULT_REQUEST_HEADERS, and a single request can carry its own headers={...}.
Show 6 more questionsShow fewer questions
How do I change the user agent in Postman?
Add a User-Agent key on the request's Headers tab; your value replaces the auto-generated PostmanRuntime/<version> header. The Postman Console shows the headers each request actually sent, so you can check the change there.
Should I use random user agents for scraping?
Not per request. Pick a few real, current browser strings and keep one for a whole session or sticky IP, with the headers that browser really sends; a new random string on every request from the same IP and cookies looks less human, not more.
Is the order of headers important?
To some anti-bot systems, yes: browsers send headers in a fixed order, and a different order hints at a script. Most HTTP libraries can't reproduce a browser's order or its HTTP/2 framing exactly, so a real browser is the safer choice on strict sites.
Can headers fix a TLS mismatch?
No. Headers travel inside the TLS connection, and the TLS handshake has its own fingerprint that shows which library made it. A Chrome user agent on a Python TLS handshake is still a mismatch; you need a browser or a client that imitates the browser's handshake.
Does an HTTP proxy hide my headers?
No, it forwards them. Over HTTPS the proxy only tunnels encrypted bytes and can't read or change your headers; over plain HTTP some proxies add headers such as Via or X-Forwarded-For, which reveal the proxy.
Should Accept-Language match the proxy country?
Yes, when you scrape by country. Set Accept-Language, and in a browser the locale and time zone, to the proxy's country, so a German IP doesn't arrive with only US English; with ProxyField you pick the country, region or city per request.
Related pages
Easy Trial: 1 GB for $2.95.
Try for the price of 1 GB, no subscription, traffic never expires.