Docs menu
Docs · Error guide

403 Forbidden When Scraping Through a Proxy

A 403 Forbidden when scraping means the server understood your request and refused it, almost always because something about the request looks automated: headers that don't match a real browser, a page that needs JavaScript, too many requests, or an IP address or network the site distrusts. Find which one by comparing your scraper with a real browser on the same IP, then fix headers first, the browser second and the IP last. When the IP is the cause, ProxyField residential proxies give you another IP type to test, from $1.95/GB with a 99.6% success rate according to our service data; try them on your target 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

Seeing a Cloudflare page with the 403? Go to Cloudflare 403 Forbidden, which reads the Cloudflare-specific signals.

On this page 7 sections

What does 403 Forbidden mean when scraping?

A 403 is a refusal, not a failure. RFC 9110, section 15.5.4 says the server "understood the request but refuses to fulfill it", and that a server which wants to hide why may answer 404 instead. For scrapers, the refusal usually comes from one of three places:

Who refusesHow to recognize itTypical reason
A bot-protection or firewall layer in front of the site (a CDN, a WAF, a bot manager)A branded block page, a reference or incident ID, cookies or headers you don't see on normal pagesThe request looks automated: headers, fingerprint, rate, IP reputation
The website itselfThe site's own 403 pageYour IP, country or account is blocked, or the page needs a login
The proxyA 403 that comes back for one target from every exit IP, in the proxy's own formatThe proxy service blocks that site category

The last row is rare but worth ruling out. ProxyField restricts some site categories to protect the network, including financial and government services, but its gateway has no 403 or other response of its own for a restricted target. A 403 through ProxyField comes from the site and is passed through.

Why does my scraper get a 403 when the page works in my browser?

Your browser and your scraper send very different requests, even from the same IP:

  • Headers. A browser sends a full, consistent set: User-Agent, Accept, Accept-Language, Accept-Encoding, Sec-Fetch-* and, in Chrome, client hints. An HTTP library sends a few defaults, and a default User-Agent such as python-requests/2.x is easy to refuse.
  • Cookies and JavaScript. Many sites set a cookie through JavaScript on the first visit and expect it on every later request. A scraper that doesn't run JavaScript never gets it.
  • TLS and HTTP/2 fingerprint. The way a client opens the encrypted connection identifies its library, whatever User-Agent it claims.
  • Rate and pattern. A browser loads one page every few seconds; a scraper loads hundreds a minute, in order, with no assets.

If the page loads in a normal browser over the same proxy IP, the IP is fine and your client is the problem. If it fails in the browser too, the IP or the network it belongs to is the problem.

How to fix a 403 Forbidden error when scraping

Change one thing at a time and re-test, simplest first.

1. Send complete, consistent browser headers

Copy the headers a real browser sends to that site (the browser's developer tools show them) and send the same set, in a consistent combination: a Chrome User-Agent with Chrome's Accept values, not a mix. Web scraping headers lists what a real browser sends and what to copy. Add the Referer the site would see when a user clicks through.

2. Keep cookies across requests

Use a session object (requests.Session(), a Scrapy cookie jar, one browser context) so the cookies the site sets on the first page go back on the next ones.

3. Use a real browser for pages that need JavaScript

If the HTML you get is a near-empty shell or a challenge page, load it with Playwright, Puppeteer or Selenium and keep one browser context per session. Setup with proxies: Playwright, Puppeteer, Selenium.

4. Slow down

A 403 that starts after a burst, or after a run of 429s, means a rate rule escalated to a block. Lower concurrency per domain, add random delays and back off on errors; the 429 Too Many Requests doc has a retry loop.

5. Change the IP type, and use sticky sessions for flows

If a browser on your current IP is refused too, the site distrusts the IP or its network. Datacenter ranges are the usual target; move to residential proxies, mobile proxies or static residential (ISP) proxies. Use a new IP per request for independent pages, and one sticky IP (30 s to 36 h) for logins, carts and multi-page flows, so cookies and IP stay together. Choose the country the site serves: country and city targeting are part of the username, as in <login>-country-us-city-new_york. Region and session syntax: -region-<region>, -session-<id> and -ttl-<seconds> (30 to 129,600 seconds, 20 minutes by default) after the location, as in <login>-country-us-region-new_york-city-new_york-session-he9f2qga-ttl-600.

6. Use an API where the site offers one

An official API or data export is the most stable route. Otherwise, ProxyField's Web Scraping API fetches any URL as plain HTTP or with JavaScript rendering, billed on successful results only; contact sales for access.

7. If the block is deliberate, stop

If every client and IP you try gets a 403, the site has decided not to serve automated traffic. Read its terms and robots.txt, and ask for access. Getting past a barrier set up on purpose can raise legal risk: see is web scraping legal.

Test a 403 through the proxy

This Python snippet sends the same URL twice through the ProxyField gateway, once with library defaults and once with browser-like headers in a session, and prints both status codes and the page title. Replace <login>, <password> and the URL.

Python
import re

import requests

PROXY = "http://<login>-country-us-city-new_york:<password>@gate.proxyfield.io:7000"
PROXIES = {"http": PROXY, "https": PROXY}
URL = "https://example.com/"

HEADERS = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
                  "(KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Language": "en-US,en;q=0.9",
    "Accept-Encoding": "gzip, deflate",
}


def title(html):
    m = re.search(r"<title>(.*?)</title>", html, re.I | re.S)
    return m.group(1).strip() if m else None


plain = requests.get(URL, proxies=PROXIES, timeout=30)
print("defaults:", plain.status_code, title(plain.text))

with requests.Session() as s:
    s.headers.update(HEADERS)
    s.proxies.update(PROXIES)
    browserlike = s.get(URL, timeout=30)
    print("browser-like:", browserlike.status_code, title(browserlike.text))

If only the second request passes, headers were the cause. If both fail, try a real browser on the same proxy IP (fix 3), then another IP type (fix 5).

How to fix 403 Forbidden in Scrapy

Scrapy's HttpErrorMiddleware drops non-2xx responses before your callback sees them, so a 403 shows up only as a log line. To read the block page, let 403 through for that spider (Scrapy docs: HttpErrorMiddleware):

Python
import scrapy


class ProductSpider(scrapy.Spider):
    name = "products"
    handle_httpstatus_list = [403]
    custom_settings = {
        "USER_AGENT": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
                      "(KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
        "DEFAULT_REQUEST_HEADERS": {
            "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
            "Accept-Language": "en-US,en;q=0.9",
        },
        "AUTOTHROTTLE_ENABLED": True,
    }

    def start_requests(self):
        yield scrapy.Request(
            "https://example.com/",
            meta={"proxy": "http://<login>-country-us-city-new_york:<password>@gate.proxyfield.io:7000"},
        )

    def parse(self, response):
        if response.status == 403:
            self.logger.warning("403 on %s: %s", response.url, response.css("title::text").get())
            return
        yield {"title": response.css("title::text").get()}

Scrapy's default User-Agent names Scrapy, so setting a browser one is the first fix. Full proxy setup: Scrapy.

403 vs 401, 407 and 429

CodeMeaningWhat fixes it
401 UnauthorizedThe site wants you to log inValid credentials for the site
403 ForbiddenThe site refuses the requestChange what its rule checks: headers, browser, rate, IP
407 Proxy Authentication RequiredThe proxy wants your proxy loginThe right proxy username and password: 407 Proxy Authentication Required
429 Too Many RequestsA rate limitSlow down and wait

A 403 that persists for one IP is also covered in why is my IP address blocked.

403 Forbidden When Scraping Through a Proxy FAQ

Why does my scraper get a 403 while the page loads fine in my browser?

ProxyField residential proxies start at $1.95/GB, but a page that loads in your browser and not in your scraper usually points at your client, not the IP: the site is refusing the scraper's headers, cookies, JavaScript or TLS fingerprint. Test it with 2 requests over the same proxy IP, one from a normal browser and one from the scraper; if both fail, change the IP type.

What causes a 403 error when scraping?

A rule on the site, or a bot-protection layer in front of it, decided the request looks automated or unwelcome. The usual triggers are default library headers, missing cookies or JavaScript, a fast request rate, and an IP or network with a poor reputation.

How do I fix a 403 Forbidden error when scraping?

Fix the simplest cause first: send complete browser headers, keep cookies in a session, render JavaScript pages in a real browser, slow down, and only then change the IP type. If every client and IP is refused, the block is deliberate; ask the site for access.

Does a 403 error mean my IP is banned?

Not necessarily. Most scraper 403s are about the request, not the IP; test the same page in a browser on the same IP to tell which one it is.

Why do I get 403 errors only through a proxy?

The site distrusts the proxy IP or its network, often a datacenter range, or it serves only some countries and your exit is elsewhere. Try a residential or mobile IP in a country the site serves, and keep the same headers you use without the proxy.

Does a 403 error go away on its own?

Sometimes: a block triggered by a burst of requests may lift after minutes or hours. A 403 caused by your headers, client or IP type stays until you change that.

How do I fix 403 Forbidden in Scrapy?

Set a real browser USER_AGENT and default headers, turn on AutoThrottle, and add handle_httpstatus_list = [403] to read the block page. If the page needs JavaScript, render it with a browser integration; if the IP is refused, set a residential proxy in request.meta["proxy"].

Why do I still get 403 Forbidden despite rotating IPs and random user agents?

Random user agents often make it worse: a Chrome User-Agent on a Python client contradicts its other headers and TLS fingerprint. Send one consistent browser profile, keep cookies in sessions and use a real browser where the site checks JavaScript.

Do I need rotating proxies for every 403?

No. Rotation helps when the site blocks or rate-limits per IP; it does nothing for header, cookie, JavaScript or fingerprint checks, which cause most scraper 403s.

What is the difference between a 401 and a 403 in web scraping?

A 401 asks you to log in to the site, and valid credentials fix it. A 403 refuses the request even if you are logged in, so you need to change what the site's rule checks, or stop.

Show 2 more questionsShow fewer questions

What does "403 Forbidden - rate limit exceeded" mean?

The server enforces a rate limit and answers with 403 instead of the usual 429. Treat it like a 429: wait, slow down, back off on each error, and spread requests over more IPs only if the limit counts IPs.

How can I prevent 403 errors at scale?

Keep each request consistent with one real browser, pace requests per domain, reuse cookies through sessions, and match the IP type and country to the site. Watch the share of 403s per target and slow down as soon as it rises.

Easy Trial: 1 GB for $2.95.

Try for the price of 1 GB, no subscription, traffic never expires.