Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A requests.exceptions.TooManyRedirects error means Requests followed more redirects than its configured limit. It does not, by itself, mean the network is down. To find the cause, inspect the redirect chain—especially each response’s Location header—then correct the URL or server, proxy, cookie, or authentication rule that is sending the request around a loop. Use allow_redirects=False to expose the first hop, and keep a timeout on diagnostic requests.

What the error means—and what it does not

Requests follows redirects automatically for most methods. If the number of redirects reaches the configured maximum, it raises requests.exceptions.TooManyRedirects. The Requests API documents a default limit of 30 through requests.models.DEFAULT_REDIRECT_LIMIT; the implementation raises when the recorded redirect history reaches the session’s limit. See the Requests API reference and Requests quickstart.

This is a guardrail against an excessive redirect chain, not a diagnosis of its cause. A genuine cycle such as A → B → A can trigger it, but so can a long, finite chain. Common possibilities include conflicting HTTP/HTTPS or www/apex-host rules, trailing-slash rewrites, cookie-dependent routing, and authentication flows. Treat these as hypotheses: the actual response URLs and Location headers show what your request received.

A timeout serves a different purpose. It bounds how long Requests waits for a response; it does not set the redirect limit or repair a loop. Requests recommends timeouts for nearly all production requests in its quickstart.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Diagnose the redirect chain

Catch the exception and inspect the available response

Reproduce the request with a bounded timeout. If it raises TooManyRedirects, the exception may carry the response at which the redirect limit was reached. When a request completes, response.history contains redirect responses in oldest-to-newest order. Log the status, URL, destination, and—when relevant—cookie information rather than only printing the exception.

import requests

url = "https://example.com/start"
try:
    response = requests.get(url, timeout=(5, 20))
except requests.exceptions.TooManyRedirects as exc:
    response = exc.response
    print("redirect limit reached")
    if response is not None:
        print("last URL:", response.url)
        for item in response.history:
            print(item.status_code, item.url, "->", item.headers.get("Location"))
else:
    print("final:", response.status_code, response.url)
    for item in response.history:
        print(item.status_code, item.url, "->", item.headers.get("Location"))

The exception’s response and history can help, but a no-follow request is often the clearest way to begin: it returns the first redirect response without following it.

Stop after the first redirect

For GET, pass allow_redirects=False. Print the response status, the URL Requests requested, and the Location header; that header identifies the next destination proposed by the server.

import requests

r = requests.get(
    "https://example.com/start",
    allow_redirects=False,
    timeout=(5, 20),
)
print(r.status_code, r.url, r.headers.get("Location"))

Requests documents allow_redirects for GET, OPTIONS, POST, PUT, and DELETE. By default it follows redirects for all verbs except HEAD. If you are diagnosing a different method, set the option on that request and be mindful that redirect behavior can interact with the method and request body. See the quickstart.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Follow the chain deliberately

Repeat the no-follow request against each destination to map the path. A small loop can collect the chain while leaving every redirect visible. This example is for a GET flow; it stops when the response is not a redirect or when it revisits a URL, and it has a hop cap as a safety guard. It does not replace the timeout.

import requests

url = "https://example.com/start"
seen = set()
max_hops = 40

with requests.Session() as session:
    for hop in range(max_hops):
        if url in seen:
            print("URL repeated; likely redirect cycle:", url)
            break
        seen.add(url)

        response = session.get(
            url,
            allow_redirects=False,
            timeout=(5, 20),
        )
        location = response.headers.get("Location")
        print(hop + 1, response.status_code, response.url, "->", location)

        if response.is_redirect or response.is_permanent_redirect:
            if not location:
                print("Redirect response has no Location header")
                break
            url = requests.compat.urljoin(response.url, location)
        else:
            print("stopped at", response.status_code, response.url)
            break
    else:
        print("stopped at diagnostic hop cap")

Redirect destinations may be relative paths, so the example resolves each Location against the response URL. If cookies, headers, authentication, or a particular HTTP method are part of the original request, include the relevant conditions in your diagnostic request; a chain can change when those conditions change.

Find and fix the loop

Compare each response’s Location with the URL that produced it. Identify the first repeated URL or conflicting transformation, then change the rule that emits the bad destination. Where the redirect rule lives depends on the application’s deployment; check the component whose response contains the problematic Location.

  • A → B → A: Two destinations are redirecting back to each other. Check both endpoints’ redirect rules and make one destination canonical.
  • HTTP ↔ HTTPS: A client, application, or proxy may think the request uses a different scheme from the one seen by another layer. Inspect the actual Location values and the reverse-proxy or application scheme-handling configuration.
  • www ↔ apex host: Conflicting canonical-host rules can send the request between the two hostnames. Choose one canonical host and align the rules that redirect to it.
  • Trailing slash changes: A slash-adding rule and a slash-removing rule can undo each other. Check route and rewrite rules for the exact paths in the chain.
  • Cookies or authentication: A site may redirect to sign-in, back to the requested page, or to a consent or session route repeatedly when expected cookies or credentials are absent or rejected. Inspect relevant cookie and authentication behavior, and confirm what the server receives.
  • Client-built URL: The initial URL may already be wrong or may be constructed differently than expected. Print it before sending the request and test the intended canonical destination directly.

After correcting the responsible rule, request the canonical final URL directly and confirm it returns the expected response. Keep a finite redirect chain if it is intentional; eliminate the cycle rather than merely hiding it from the client.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

When to use allow_redirects=False or raise the limit

These options answer different questions. Use allow_redirects=False to inspect a redirect response without automatically following it. Raise the limit only when you know the chain is finite and legitimately longer than the current ceiling. Neither setting fixes a server-side or client-side loop.

Choice Use it when Trade-off
allow_redirects=False You need to see the first 3xx status and its Location destination. Your code must decide what to do next; automatic following is disabled for that request.
Inspect response.history The request completed and you need the redirects Requests followed. It is a trace of a completed request, so use a no-follow request if you need to examine the first hop before following.
Increase Session.max_redirects A known, finite workflow needs more hops than the current configured limit. It can delay failure on a cycle and does not correct the rule creating it.

For a deliberate session-wide ceiling, Requests exposes Session.max_redirects. Choose a value based on the finite chain you expect; do not set an unlimited retry policy as a substitute for finding the cause.

import requests

session = requests.Session()
session.max_redirects = 10  # choose deliberately; this is a guardrail, not a loop fix

response = session.get("https://example.com/start", timeout=(5, 20))

The documented default is 30, but applications can set a different session value. The API reference describes the configurable maximum and the default constant; the implementation’s check explains why reaching the ceiling raises the exception (API reference; Requests implementation).

Common symptoms and fixes

  • The no-follow request returns a 3xx: This is useful diagnostic output, not necessarily an error. Read Location, request that destination with redirects disabled, and continue until you reach a non-redirect response or a repeated URL.
  • response.history is empty: A response may not have completed through automatic redirect following, or the redirect handling was disabled. Use the exception’s attached response if available, or inspect each hop with allow_redirects=False.
  • The browser works but Requests loops: Browser and script requests may differ in cookies, authentication, headers, or other session state. Compare the actual chain and the conditions of each request; do not assume browser behavior proves the Python request is equivalent.
  • The exception response is None: There may be no response attached to inspect in that case. Start again with a no-follow request and bounded timeout to capture the first redirect explicitly.
  • A timeout occurs instead: A timeout and a redirect-limit exception are different failures. Keep the timeout bounded, then determine whether a slow response or connection issue is occurring rather than assuming the redirect chain caused it.
  • Raising the ceiling only makes the error take longer: Revert the change unless you have confirmed a finite chain that needs the extra hops. Trace the destinations and repair the loop.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your goal is a clean screenshot rather than debugging a Python redirect chain, ScreenshotNeo provides a one-call website screenshot API. It accepts cookie and consent banners like a visitor and removes 60+ known consent platforms, newsletter popups, and chat widgets before capture; those cleanup steps can be turned off. Bot checks/CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and responses identify the page verdict and billing status in headers. ScreenshotNeo also has an MCP server for AI agents, with take_screenshot, get_page_info, and capture_pdf.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use the API key from your ScreenshotNeo account. The API returns an image (PNG, JPEG, or WebP) or PDF; this example saves the returned image bytes as shot.webp.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp

See the ScreenshotNeo API documentation for request options. 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000. Sign up for free ScreenshotNeo screenshots.

Frequently asked questions

Does TooManyRedirects mean the site is down?

No. It means the configured redirect ceiling was reached. Inspect the chain to determine whether the destination rules are cyclic or the chain is simply longer than expected.

Can I disable redirects for a POST request?

Yes. Requests documents allow_redirects` for POST as well as GET, OPTIONS, PUT, and DELETE. Set it on the request when you need to inspect the redirect response rather than follow it automatically.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Is the default maximum always 30?

The Requests API documents the default constant as 30, but a session’s max_redirects can be configured. Check the session used by your code.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.