← Catalog Matrix

Deployment Execution Blueprint

---
title: How to Check Broken Links on a Website Programmatically Using Python
description: A lightweight, multi-threaded Python script blueprint to crawl a website domain, check HTTP status codes, and output all broken 404 links.
category: Python / DevOps
slug: check-broken-links-python
keywords: check broken links python, python broken link checker, web crawler 404, website link validator script, python requests beautifulsoup
---

A broken link (404 Not Found) harms user experience and degrades your website's search engine indexing status. Manually hunting for dead links across hundreds of static flat-files or dynamic routes is highly inefficient, and many cloud-based auditing tools force you into premium monthly tiers.

Building your own bare-metal link crawler in Python provides an automated, free way to audit your technical SEO architecture.

## How the Auditing Engine Works

To efficiently map a site without getting blocked or bottlenecked, a program must execute four specific phases:
1. **Domain Locking:** The script normalizes all crawled URLs and ensures it stays locked to your root domain, avoiding external resource blackholes.
2. **Asynchronous/Multi-threaded State Verification:** It maintains an internal memory matrix of `visited_urls` and `discovered_links` to prevent infinite loops caused by cyclical navigation menus.
3. **DOM Content Scraping:** It parses the HTML structure specifically targeting the standard markup link anchors (`<a href="...">`).
4. **Network Response Extraction:** It fires an HTTP `HEAD` request (which downloads only the server headers rather than the entire page layout) to lightning-fast determine if a status code returns `200 OK` or `404 Broken`.

The Python automation blueprint below utilizes `requests` for fast connection handling and `BeautifulSoup` for clear DOM parsing to build a multi-threaded link validator.

# Python Broken Link Checker Automation Script

import sys
import threading
from queue import Queue
from urllib.parse import urljoin, urlparse
import requests
from bs4 import BeautifulSoup

class LinkCrawler:
    def __init__(self, base_url, max_threads=10):
        self.base_url = base_url
        self.domain = urlparse(base_url).netloc
        self.visited_urls = set()
        self.broken_links = []
        self.queue = Queue()
        self.max_threads = max_threads
        self.lock = threading.Lock()
        
        # Seed the queue with the entry point
        self.queue.put(base_url)

    def is_internal(self, url):
        """Ensure the script stays locked to the target domain."""
        return urlparse(url).netloc == self.domain

    def check_url_status(self, url):
        """Fire a fast HEAD request to evaluate link validity."""
        try:
            # Use HEAD request for speed; fallback to GET if headers are restricted
            response = requests.head(url, timeout=5, allow_redirects=True)
            if response.status_code >= 400:
                # Retry with GET just in case the server blocks HEAD requests
                response = requests.get(url, timeout=5, stream=True)
            return response.status_code
        except requests.RequestException:
            return 999  # Connection error code replacement

    def crawl_worker(self):
        while True:
            try:
                url = self.queue.get(timeout=2)
            except:
                break

            with self.lock:
                if url in self.visited_urls:
                    self.queue.task_done()
                    continue
                self.visited_urls.add(url)

            print(f"[Auditing]: {url}")
            status = self.check_url_status(url)

            if status >= 400:
                with self.lock:
                    self.broken_links.append({"url": url, "status": status})
            else:
                # If internal and valid, extract child links to expand search depth
                if self.is_internal(url) and status == 200:
                    try:
                        response = requests.get(url, timeout=5)
                        soup = BeautifulSoup(response.text, 'html.parser')
                        for anchor in soup.find_all('a', href=True):
                            href = anchor['href']
                            full_url = urljoin(url, href)
                            # Remove trailing fragments or queries for normalization
                            clean_url = full_url.split('#')[0]
                            
                            with self.lock:
                                if clean_url not in self.visited_urls:
                                    self.queue.put(clean_url)
                    except Exception:
                        pass

            self.queue.task_done()

    def run(self):
        threads = []
        for _ in range(self.max_threads):
            t = threading.Thread(target=self.crawl_worker)
            t.daemon = True
            t.start()
            threads.append(t)

        self.queue.join()
        
        print("\n--- AUDIT COMPLETE ---")
        if not self.broken_links:
            print("Success: Zero broken links found across the domain matrix.")
        else:
            print(f"Alert: Found {len(self.broken_links)} broken connections:\n")
            for item in self.broken_links:
                print(f"Status Code [{item['status']}] -> Dead Reference: {item['url']}")

if __name__ == "__main__":
    # Replace with the domain you intend to programmatically inspect
    TARGET_SITE = "https://example.com" 
    
    crawler = LinkCrawler(TARGET_SITE, max_threads=12)
    crawler.run()