Broken links quietly hurt a site. Readers hit dead ends, and search engines waste crawl time on pages that don't exist. Most broken-link checkers are browser extensions that check one page, or SEO suites that cost $100+ a month.
Here's a free Python script that checks every link on a page, using only the standard library, no installs. After that, a no-code option for checking a whole site.
The script
It does three things:
- Downloads the page and pulls out every
<a href>. - Checks each link with a
HEADrequest, falling back toGETbecause some servers refuseHEAD. - Prints anything that returns 4xx/5xx or doesn't connect at all.
"""Find broken links on one web page. Usage: python3 find_broken_links.py https://example.com"""
import sys
import urllib.error
import urllib.parse
import urllib.request
from concurrent.futures import ThreadPoolExecutor
from html.parser import HTMLParser
UA = {"User-Agent": "Mozilla/5.0 (link checker)"}
class Links(HTMLParser):
def __init__(self):
super().__init__()
self.hrefs = []
def handle_starttag(self, tag, attrs):
href = dict(attrs).get("href")
if tag == "a" and href:
self.hrefs.append(href)
def status(url):
for method in ("HEAD", "GET"): # some servers refuse HEAD
try:
req = urllib.request.Request(url, method=method, headers=UA)
with urllib.request.urlopen(req, timeout=15) as r:
return r.status
except urllib.error.HTTPError as e:
if method == "GET" or e.code not in (403, 405, 501):
return e.code
except Exception as e:
return f"error: {getattr(e, 'reason', e)}"
page = sys.argv[1]
html = urllib.request.urlopen(urllib.request.Request(page, headers=UA), timeout=15).read().decode("utf-8", "replace")
parser = Links()
parser.feed(html)
links = {urllib.parse.urldefrag(urllib.parse.urljoin(page, h))[0] for h in parser.hrefs}
links = sorted(u for u in links if u.startswith("http"))
with ThreadPoolExecutor(10) as pool:
results = list(zip(links, pool.map(status, links)))
broken = [(u, s) for u, s in results if not isinstance(s, int) or s >= 400]
print(f"Checked {len(links)} links on {page}: {len(broken)} broken")
for u, s in broken:
print(f" {s} {u}")
Run it:
python3 find_broken_links.py https://www.python.org/about/apps/
Real output (October 2026):
Checked 119 links on https://www.python.org/about/apps/: 3 broken
error: [Errno 61] Connection refused http://mcsp.wartburg.edu/zelle/python/
error: [Errno 61] Connection refused http://pragprog.com/book/gwpy2/practical-programming
404 http://wiki.python.org/moin/TkInter
Even python.org has a few. Old links rot: domains expire, wikis move, and http:// links stop answering.
Things that will trip you up
- False alarms from social sites. LinkedIn, X, and Instagram often answer automated requests with 999, 403, or 520 even when the page works fine in a browser. Treat those as "check by hand", not "broken".
-
Redirects.
urllibfollows them silently. A link that redirects to a 404 shows as 404, which is what you want. A long redirect chain is still worth fixing. - JavaScript sites. If a site builds its pages with JavaScript, the raw HTML has no links in it. You'd need a headless browser.
-
Be polite. Ten threads is fine for one page. For a whole site, limit requests per server and respect robots.txt (
urllib.robotparserexists, but it mishandles*wildcards and some?rules, so test it).
Checking a whole site (no code)
The script checks one page. For a full site you need the list of pages (from the sitemap), deduplication across pages, "which page is this broken link on?", and the false-alarm handling above.
I packaged all of that as a Broken Link Checker on Apify. Type a domain and it reads the sitemap (or follows internal links), checks every link, marks bot-blocking sites as restricted instead of broken, and tells you which page each broken link is on. It costs $0.80 per 1,000 links checked, with no subscription, and you can schedule it weekly. On python.org, 30 pages and 2,000 links took about a minute and found 4 broken links. It also respects each site's robots.txt, so it skips what sites ask crawlers not to touch, and doesn't charge for those.
If you also want titles, meta descriptions, and H1s checked, the same idea powers a Website SEO Audit that scores each page and lists fixes.
Either way, the script above is yours. Copy it, change it, run it on a schedule.
Top comments (0)