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

Fix it by finding the redirect loop, not by simply raising the limit. Catch requests.exceptions.TooManyRedirects, inspect the chain with response.history, and make a one-hop request with allow_redirects=False. The repeated Location values usually reveal a bad URL, HTTP/HTTPS bounce, host mismatch, rewrite rule, or authentication-cookie loop. Correct that rule or call the canonical URL directly; increase Session.max_redirects only for a known, finite redirect sequence.

What the exception means

Requests follows redirects automatically for GET, OPTIONS, POST, PUT and DELETE. If the chain reaches the configured ceiling, it raises TooManyRedirects. The exception is a client-side guardrail: it does not prove that the server is down or that the network is unavailable.

The documented default maximum is 30 redirects. Requests records each redirect response in Response.history, ordered from the oldest hop to the newest. A timeout is a separate safeguard and should still be supplied on production requests.

Diagnose the chain before changing settings

Reproduce with a bounded timeout

Use a connect/read timeout tuple so a broken endpoint cannot hold the process indefinitely. When the limit is reached, the exception often carries the last response in exc.response.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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"),
        )

Log the status code, source URL, Location header and any relevant cookies for every hop. Do not log authorization tokens or session secrets; redact those values before sending diagnostics to a shared log.

Expose the first hop

Disable following temporarily to see exactly what the starting URL returns:

import requests

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

A 301, 302, 303, 307 or 308 response with a Location header is the next hop. Follow that URL manually with the same diagnostic request, or use the logging loop below to inspect a bounded number of hops without allowing an unbounded client loop.

Print a bounded redirect trace

import requests

session = requests.Session()
current = "https://example.com/start"
seen = set()

for hop in range(15):
    if current in seen:
        print("cycle detected:", current)
        break
    seen.add(current)

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

    if r.status_code not in (301, 302, 303, 307, 308) or not location:
        print("chain ends here")
        break

    current = requests.compat.urljoin(current, location)
else:
    print("diagnostic hop limit reached")

This trace is deliberately finite. It lets you see a cycle while protecting a script from endlessly issuing requests.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

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

Recognize the common loop patterns

A URL cycle (A → B → A)

If two or more URLs repeat, compare each server’s Location value. A rewrite, canonical-link rule or application redirect is sending the client back to a URL it already visited. Remove one of the competing rules or make both rules converge on one canonical URL.

HTTP and HTTPS bouncing

A TLS terminator or reverse proxy may tell the application that the request is HTTP even though the visitor used HTTPS. The application then redirects to HTTPS, while the proxy redirects the resulting request back to HTTP. Fix the proxy’s forwarded-protocol configuration and the application’s “secure request” detection. Test the public URL, not only an internal service address.

www and apex-host disagreement

For example, www.example.com can redirect to example.com while another rule sends the apex host back to www. Choose one host, issue one permanent redirect to it, and remove the reverse rule. Check ports as well: an implicit HTTP-to-HTTPS redirect can combine with a host rewrite to create a longer cycle.

Trailing-slash and path canonicalization

Framework routing may add a slash, while a proxy or CMS removes it. Inspect the exact paths, case, URL encoding and query string in every hop. Configure canonicalization in one layer, or make the layers agree on the same form.

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

Authentication and cookie redirects

A login endpoint can redirect to a protected page, which redirects back to login when the session cookie is missing, rejected or scoped to the wrong domain/path. Inspect Set-Cookie and the cookies sent on the next request. Check secure and SameSite attributes, domain and path scope, clock skew for signed cookies, and whether your session is being discarded between calls. A redirect caused by an expired login is not fixed by raising the redirect limit; repair the authentication flow or authenticate the request correctly.

Apply the durable fix

Call the canonical URL directly

Once the chain identifies the intended final address, use that address in your code. This avoids unnecessary hops but does not replace correcting a server rule that other clients still encounter.

Correct the emitting layer

  • Client URL construction: remove accidental scheme, host, slash or query transformations.
  • Web server: consolidate rewrite and canonical-host rules.
  • Reverse proxy/CDN: pass the original host and scheme consistently to the application.
  • Application: return one canonical redirect and avoid redirecting an already-canonical request.
  • Authentication: preserve the session cookie and verify login return URLs.

After changing a rule, repeat the no-follow test and then a normal request. Confirm that the chain terminates at the expected status and URL from the same environment where your Python process runs.

Use max_redirects only for a finite chain

import requests

session = requests.Session()
session.max_redirects = 10  # deliberate guardrail for a known finite chain
response = session.get("https://example.com/start", timeout=(5, 20))
print(response.status_code, response.url)

Raising the ceiling can be reasonable when a legacy sign-in flow legitimately requires more than the default number of hops. It only delays the exception if a cycle exists, increases latency and load, and can hide a configuration defect. Never treat a very large value as a loop fix, and do not remove timeouts.

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

Choose the right redirect behavior for each request

Goal Setting or method What you learn
Inspect the first response allow_redirects=False Status, first Location, cookies and headers
Follow a normal finite chain Default behavior Final response plus response.history
Support a known longer chain Session.max_redirects = n A higher, still finite guardrail
Prevent a hanging request timeout=(connect, read) Separate protection from redirect limits

Troubleshooting checklist

  • exc.response is None: retain the original URL and run a separate allow_redirects=False request; the exception did not provide a response object.
  • No Location header appears: verify that you are examining the redirect response, not a later response, and print headers with sensitive values removed.
  • The browser works but Requests loops: compare cookies, authorization, user agent and proxy settings. A browser may already hold a valid session or use a different proxy path.
  • Only POST loops or changes method: inspect 301/302/303 behavior and test the endpoint’s intended 307/308 semantics. Confirm that the server’s redirect target accepts the resulting method.
  • Redirects differ by environment: compare DNS, proxy, CDN, forwarded headers, host and scheme. The public edge and internal test URL may not use the same rules.
  • It times out instead of raising: set a connect/read timeout. A slow origin and a redirect loop are different failures and need separate measurements.

Or skip the browser setup

If your goal is a clean image or PDF of a page rather than debugging its redirect policy, ScreenshotNeo provides a single HTTP request. It accepts cookie and consent banners as a visitor, then removes more than 60 known consent platforms, newsletter popups and chat widgets before capture; each step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the page verdict and billing result.

Use the API documentation at https://screenshotneo.com/docs/ for all options. A cURL request:

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

Python:

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo also offers an MCP server with take_screenshot, get_page_info and capture_pdf tools for Claude, Cursor and other MCP clients. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

FAQ

Does TooManyRedirects always mean a server bug?

No. The loop can come from the URL your code builds, a proxy or CDN, an application rewrite, or an authentication cookie problem. The observed Location chain identifies which layer to investigate.

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

Can I disable redirects permanently?

You can pass allow_redirects=False per request, but that changes the response your application receives. Use it for diagnostics or when your code intentionally handles each hop; do not use it to conceal a broken canonical URL.

What is the difference between response.url and response.history?

response.url is the URL of the response you hold, usually the final URL. response.history contains the earlier redirect responses in chronological order, allowing you to reconstruct the path.

Should I retry after this exception?

Not automatically. A retry repeats the same redirect policy and can add load. First determine whether the chain is deterministic; retry only after correcting the URL, session or server rule, with a bounded timeout and finite redirect limit.

Frequently Asked Questions

How many redirects does Requests allow by default?

The documented default is 30 redirects per request. The limit is a guardrail and is not evidence that 30 hops are valid for your endpoint.

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

How can I see the very first redirect?

Send the request with allow_redirects=False and print status_code, url and headers.get("Location").

Is a timeout an alternative to a redirect limit?

No. A timeout bounds waiting for a connection or response; the redirect limit bounds how many responses Requests will follow. Production code generally needs both.

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.