A 503 means a server will not handle your request right now. Through a proxy, four kinds of machine can send it. The proxy itself can, when it cannot reach the site. A rate limiter can, when your address has used up its share. The site's edge can, and so can the site, when it is overloaded or down for maintenance.
Each one has a different fix, so the first job is to learn who answered. Two clues settle it. Each server writes its 503 page in its own words. And over https, your client reports a proxy's 503 in a different way from a site's. We measured that second clue on October 23, 2026, with curl, GNU Wget and Python requests against stub servers on our own machine.
What a 503 means
RFC 9110, the HTTP standard, defines 503 as a server that "is currently unable to handle the request due to a temporary overload or scheduled maintenance, which will likely be alleviated after some delay". The server may also send a Retry-After header to say how long to wait.
MDN adds a point worth keeping. A server that limits one client's rate should answer 429 Too Many Requests, it says. nginx's own rate limiter answers 503 instead, unless its owner sets another code.
Who sent it: read the words
Each server writes its 503 page in its own words, and people search for exactly those words:
The table pairs each wording with its author, from the server's own source code or documentation:
| The page says | Who wrote it | What it means |
|---|---|---|
| 503 Service Temporarily Unavailable | nginx | Its built-in page. Also what its rate limiter sends by default |
| HTTP Error 503. The service is unavailable. | IIS, through Windows' HTTP layer | The app pool is offline after errors, disabled, too busy, or its queue is full |
| Error 503 Backend fetch failed, with "Guru Meditation" | Varnish | The cache could not reach the server behind it |
| No server is available to handle this request. | HAProxy | No server behind the load balancer was available |
| Any 503 with "cloudflare" or "cloudflare-nginx" in the page | Cloudflare | Cloudflare says the problem may be on its side |
| A 503 with none of these | Often the site's own app | Check the headers and the timing next |
Cloudflare's rule comes from its own docs. A 503 with "cloudflare" or "cloudflare-nginx" in the HTML may stem from Cloudflare. One without those words came from the site's own server.
Over https, your client already tells you
Most scraping runs over https. Then your client asks the proxy for a tunnel with a CONNECT request, and the page travels inside that tunnel. That gives two very different 503s. The proxy can refuse the tunnel with 503. Or the tunnel opens and the site answers 503 inside it.
We built both cases on our own machine. Stub A, a proxy, answers every CONNECT with 503. Stub B opens the tunnel to a local https server that answers 503. Then we sent the same request through each with the clients already installed on three Linux systems.
Every client drew the line in the same place:
| Client | The proxy refused the tunnel with 503 | The site answered 503 inside the tunnel |
|---|---|---|
| curl 8.5.0, 8.14.1, 8.18.0 | Exit 56, "CONNECT tunnel failed, response 503" | Exit 0, status 503 |
| GNU Wget 1.21.4, 1.25.0 | Exit 4, "Proxy tunneling failed: Service Unavailable" | Exit 8, "ERROR 503: Service Unavailable." |
| GNU Wget2 2.2.1 | Exit 4, "Failed to connect: Connect error" | Exit 8, "HTTP ERROR response 503" |
| Python requests 2.31.0 | Raises ProxyError, "Tunnel connection failed: 503" | Returns a response with status_code 503 |
So the rule is simple. If your code got a response object with status 503, the site's side sent it. If your code got a proxy or tunnel error that mentions 503, the proxy sent it, and the site never saw the request. curl can print both answers in one line:
curl -s -o /dev/null -w 'tunnel %{http_connect}, page %{http_code}\n' \
-x http://YOUR-LOGIN:YOUR-PASSWORD@premium.hproxy.com:9000 https://example.com/
"tunnel 503, page 000" means the proxy refused. "tunnel 200, page 503" means the site answered 503. Curl 7.86.0 and older word the proxy's refusal as "Received HTTP code 503 from proxy after CONNECT", with the same exit code 56. Curl 8.20.0, released on April 29, 2026, kept today's words and moved the exit code to 7, so a script that checks for 56 should accept 7 as well. We read both changes in curl's own source.
For a plain http address there is no tunnel, and both 503s arrive as ordinary responses. Then read the page words above, and look for a Via header or a proxy's name in the Server header. Our 502 guide measured those headers and the timing in the same way.
The fix for each sender
The site or its edge is busy. Wait and send less. Read Retry-After and wait at least that long; without it, back off and add a little random delay. Cut the number of parallel requests to that host until it answers again.
A rate limiter answered. The pattern gives it away: a fresh address works for a while, then gets 503 at about the same count. Slow down per address, and spread the work over more addresses. The next sections cover both.
The proxy answered. Check the proxy before blaming the site. Our proxy checker shows whether it is alive and how fast it answers. A proxy that cannot reach a site may send 503 itself. A 2017 report on the Squid users' list describes Squid answering 503 at once when the site refused its connection. For a paid proxy, read its error page, and widen your targeting if the error names a location.
Cloudflare answered. Its docs point to a connectivity problem in one of its data centers. Wait, then try again.
The site is down. If the 503 follows you through every exit and from a direct connection too, stop. Queue the work and test with one request now and then. A scraper that keeps hitting an outage pays for traffic that returns nothing.
Which clients honor Retry-After
Retry-After only helps if your client reads it. So we pointed each client at a stub that answers every request with 503 and "Retry-After: 3", and logged when each request arrived:
The full run, three retries each:
| Client and options | Requests sent | Waits between them |
|---|---|---|
| curl with no options | 1 | It reports the 503 and stops |
| curl --retry 3 | 4 | 3, 3 and 3 seconds |
| curl 8.5.0 --retry 3 -o /dev/null | 1 | Stopped with exit code 23 and no message |
| GNU Wget and Wget2, --tries=4 --retry-on-http-error=503 | 4 | 1, 2 and 3 seconds |
| requests with no retry adapter | 1 | Returns the 503 to your code |
| requests with urllib3 Retry(total=3, status_forcelist=[503]) | 4 | 3, 3 and 3 seconds, then RetryError |
| The same with respect_retry_after_header=False, backoff_factor=1 | 4 | 0, 2 and 4 seconds |
Three results matter for scrapers. curl has followed Retry-After since version 7.66.0, its manual says, and our run agreed. GNU Wget kept its own backoff whatever the header said: 1 second after the first failure, 2 after the second, up to 10 seconds by default. And curl 8.5.0, the version in our Ubuntu 24.04, stopped at the first 503 with exit code 23 when its output went to /dev/null. With a real output file it retried normally, and curl 8.14.1 and 8.18.0 retried either way.
Scrapy retries 503 by default, per its docs, which list 503 in RETRY_HTTP_CODES. The docs say nothing about Retry-After, and we did not run Scrapy. Pair its retries with a download delay or AutoThrottle, as our Scrapy guide shows.
In Python, a requests session with urllib3's Retry does the waiting for you. urllib3 honors Retry-After by default on 413, 429 and 503:
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
retry = Retry(
total=4,
status_forcelist=[503],
backoff_factor=1, # used when no Retry-After comes back
allowed_methods=["GET", "HEAD"],
)
session = requests.Session()
session.mount("https://", HTTPAdapter(max_retries=retry))
session.proxies = {"https": "http://YOUR-LOGIN:YOUR-PASSWORD@premium.hproxy.com:9000"}
response = session.get("https://example.com/page", timeout=30)
print(response.status_code)
With curl, add --retry and keep a cap on the total time. Write to a real file, which also sidesteps the 8.5.0 trap:
curl --retry 4 --retry-max-time 120 -o page.html \
-x http://YOUR-LOGIN:YOUR-PASSWORD@premium.hproxy.com:9000 https://example.com/page
When more addresses help
A rate limit tied to one address is the case where a pool of addresses helps. nginx shows how common that setup is. Its rate-limit module exists "in particular" to limit "requests coming from a single IP address", in its own words. Its example counts per client address, and its default answer to a request over the limit is 503.
Against a limit like that, find the pace at which the 503s start and stay below it on every address. Then add addresses to raise the total, rather than pushing one address harder. Our 429 guide works through the same arithmetic for sites that answer 429 instead.
Residential Premium gives each request a fresh residential IP by default. Some flows have to stay on one address. For those, a sticky line holds one exit for 3 minutes to 24 hours, as our guide to sticky and rotating sessions explains. It costs $1.49/GB for one GB, falling to $1.00/GB at 1,000 GB+.
More addresses do nothing for a site that is down, or for a 503 your proxy sends. Check those first, with the clues above.
503 next to 502 and 504
The three get confused because all three come from a server in the path. A 502 Bad Gateway means a gateway got an invalid answer from the server behind it. A 504 Gateway Timeout means the gateway got no answer in time. A 503 is a valid, prompt answer that says "not now". Our overview of every proxy error sorts all of them by which leg of the connection failed.
Sources
- RFC 9110, HTTP Semantics, sections 15.6.4 (503) and 10.2.3 (Retry-After), June 2022.
- MDN: 503 Service Unavailable, read October 23, 2026.
- Cloudflare: Error 503, read October 23, 2026.
- nginx: ngx_http_limit_req_module, and nginx's source file src/http/ngx_http_special_response.c for its 503 page, read October 23, 2026.
- HAProxy's source file src/http.c for its default 503 page, read October 23, 2026.
- Varnish Software: Troubleshooting Varnish, read October 23, 2026.
- Microsoft: Types of Errors Logged by the HTTP Server API, read October 23, 2026.
- curl man page, the --retry option, read October 23, 2026. curl's source, read the same day: lib/http_proxy.c at 7.86.0 and 7.87.0 for the change of message, lib/cf-h1-proxy.c at 8.19.0 and 8.20.0 for the change of exit code, and curl's release table for the date of 8.20.0.
- GNU Wget 1.25.0 manual, read October 23, 2026.
- urllib3: Retry, docs of version 2.8.0, read October 23, 2026.
- Scrapy: RetryMiddleware, read October 23, 2026.
- squid-users, May 30, 2017: 503 service unavailable on connection refused.
- Google Ads Keyword Planner, worldwide, English, read October 23, 2026.
- Our own run on October 23, 2026: stub servers on 127.0.0.1 under WSL, with curl 8.5.0, 8.14.1 and 8.18.0, GNU Wget 1.21.4 and 1.25.0, GNU Wget2 2.2.1, and Python requests 2.31.0 with urllib3 2.0.7. Nothing outside the machine was contacted.



