JustProxies

Patterns

Handling 403 · 429 from targets

5-min readtarget-blocks
407, 403, and 429 each come from a different place and need a different fix. 407 is always our gateway; 403 and 429 are always the target talking back. The strategies diverge from there.

Proxy errors vs target errors

We pass target responses through unchanged. The reliable way to know who generated an error: check for the X-Proxy-Reason header.

  • X-Proxy-Reason present → we generated the response (407, 451, 502, 503, 504).
  • X-Proxy-Reason absent → the target generated it (403, 429, 5xx).
distinguish.pypython
import requests

proxy = "http://USER:[email protected]:8080"
r = requests.get("https://target.com/", proxies={"http": proxy, "https": proxy},
                 timeout=20)

if "X-Proxy-Reason" in r.headers:
    print("Our error:", r.status_code, r.headers["X-Proxy-Reason"])
else:
    print("Target error:", r.status_code)

The full error reference is at /docs/errors.

407 — fixing auth

407 always means the proxy couldn't authenticate your request. Common causes:

  • Wrong credentials — copy-paste error or stale password. Regenerate credentials in the dashboard and update your config.
  • URL-encoding — if your username or password contains special characters (@, :, /, +), they must be percent-encoded in the URL form.
  • Wrong auth method — some libraries require Proxy-Authorization: Basic BASE64 instead of URL-embedded credentials. See the authentication article.
  • Order expired — if your bandwidth is exhausted, new requests get 407 until you place another order.
url-encode-special-chars.pypython
from urllib.parse import quote

user = quote("user@domain", safe="")   # %40
pwd  = quote("p@ss:w0rd!", safe="")   # %40 %3A %21
proxy = f"http://{user}:{pwd}@gw.justproxies.online:8080"

403 — target blocking

A 403 with no X-Proxy-Reason means the target rejected the request. Common reasons and fixes:

  • ASN/datacenter block — the target recognises the IP as belonging to a datacenter and blocks it. Switch from the datacenter pool to residential or mobile.
  • IP was previously flagged — on rotating pools each retry automatically pulls a fresh IP. Retry up to 3–4 times before giving up.
  • Missing headers — some targets require a realistic User-Agent, Accept-Language, or Referer. Add them.
  • TLS fingerprint detection — the target uses JA3/JA4 fingerprinting to detect automation. See TLS fingerprinting.
retry-403.pypython
import requests, time, random

PROXY = {"http": "http://USER:[email protected]:8080",
         "https": "http://USER:[email protected]:8080"}

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

def fetch(url: str, max_attempts: int = 4) -> requests.Response:
    for attempt in range(max_attempts):
        r = requests.get(url, proxies=PROXY, headers=HEADERS, timeout=20)
        if r.status_code != 403:
            return r
        # Each retry gets a different IP on rotating pools.
        wait = 0.5 * (2 ** attempt) + random.uniform(0, 0.5)
        time.sleep(wait)
    raise RuntimeError(f"Persistent 403 on {url} after {max_attempts} attempts")
A 403 that persists across 4+ retries almost always means the target rejects the pool type (datacenter vs residential), not the specific IP. Upgrade the pool before increasing retry count further.

429 — rate limited

The target is enforcing a request rate limit per IP or per session. Strategy:

  • Rotating pool: drop any sticky session token, retry immediately — the next IP starts with a fresh rate limit budget.
  • Sticky session: respect the Retry-After header if present, or back off for 10–30 seconds before retrying on the same IP.
  • Persistent 429 across many IPs: reduce concurrency. You are hitting an account-level or ASN-level limit, not a per-IP one.
handle-429.pypython
import time, requests

def fetch_with_429_handling(url: str, session: requests.Session) -> requests.Response:
    for attempt in range(5):
        r = session.get(url, timeout=20)

        if r.status_code != 429:
            return r

        # Respect Retry-After if present; otherwise exponential backoff.
        retry_after = r.headers.get("Retry-After")
        wait = float(retry_after) if retry_after else min(30, 2 ** attempt)
        time.sleep(wait)

    raise RuntimeError(f"Still rate-limited after 5 attempts: {url}")

When to upgrade the pool

Target blocks that don't clear after retries are a pool mismatch, not a configuration error. Upgrade criteria:

  • Datacenter → Residential: persistent 403 on rotating datacenter, clears after switching. The target ASN-detects datacenter IPs.
  • Residential → Mobile: persistent 403 on residential, target uses carrier detection or device fingerprinting. Mobile IPs have the highest trust score.
  • Rotating → Static: target requires a consistent IP for a trusted relationship (e.g., API key tied to IP allowlist). Static datacenter IPs are stable for a calendar month.

Pool capabilities are compared on /products.

Found a gap, or something wrong?
A real human reads support email.