How to Check and Score a Proxy List in Bulk

Ryan
Ryan
IP Proxy Research Team

A proxy list checker is useful when you have more than one endpoint to validate. Instead of testing proxies manually, the goal is to parse the entire file, remove bad or duplicate rows, test unique entries with controlled concurrency, export the results, and decide which endpoints should remain active.

This guide focuses on list-level quality control. For a single proxy route check, use the separate proxy testing workflow.

Direct Answer

To check a proxy list, normalize every row into a valid proxy URL, reject malformed entries, remove duplicates, run several requests through each unique endpoint, and export status, success rate, median latency, exit IP, errors, and test time to CSV. Then score the whole list using the same test conditions.

Key Takeaways
  • Parse and deduplicate the list before sending network requests.
  • Repeat each test so one successful response is not mistaken for stability.
  • Preserve invalid and duplicate rows in the report instead of silently dropping them.
  • Score the whole list using format quality, uniqueness, reachability, stability, and latency.

What a Proxy List Checker Should Do

A proxy list checker should turn an untrusted text file into a decision table. The output should show which rows are valid, duplicated, reachable, unstable, or failed, along with the evidence behind each result.

Table 1: Single-proxy testing compared with proxy-list checking.
Task Single Proxy Test Proxy List Checker
Main question Does this proxy work now? Is this entire list clean and stable enough to use?
Input One proxy endpoint A file, spreadsheet, or exported list
Core method One-off route and configuration validation Parsing, deduplication, repeated tests, scoring, and retesting
Useful output Visible IP and basic error Row-level CSV plus list-level quality metrics
Next action Correct one setting or credential Keep, quarantine, correct, retest, or remove groups of entries

Build a Clean proxies.txt File

Save the input as a plain-text file with one proxy per line. Explicit schemes reduce ambiguity during batch testing.

http://203.0.113.10:8080
http://username:password@proxy.example.net:8080
https://198.51.100.24:8443
proxy.example.net:3128

In the example script, an entry without a scheme is treated as an HTTP proxy. Keep unsupported formats, labels, and source notes outside the proxy URL unless your parser explicitly handles them.

Passwords containing characters such as @, :, or spaces should be URL-encoded before they are added to the file. Store proxies.txt securely because the source file may contain unmasked credentials.

Proxy list parsing and deduplication workflow
Figure 1: Parse and normalize the list before running network tests.

Deduplicate Before Testing

If one endpoint appears several times, it can waste concurrency slots and inflate the apparent size of the list. Deduplication should therefore happen before the network test.

The script below creates a normalized key from the scheme, host, port, and a short hash of the credentials. The hash distinguishes credentials without writing the password into the CSV report.

Pre-Test Cleanup Checklist
  • Ignore blank lines and comments.
  • Reject missing hosts, invalid ports, and unsupported schemes.
  • Normalize hostnames and schemes before comparison.
  • Record the source line for every valid, invalid, and duplicate row.
  • Keep the duplicate count for list-level scoring.

Batch Test the List with Python

The following standard-library script supports proxy URLs using the http:// and https:// schemes. It tests every unique proxy three times, limits concurrency, masks passwords, records failure categories, and creates two files:

  • proxy-results.csv: one row for each valid, invalid, duplicate, or tested entry
  • proxy-summary.csv: list-level rates, latency, quality score, and rating

Create proxies.txt in the same directory, then save the script as proxy_list_checker.py and run:

python proxy_list_checker.py
import csv
import hashlib
import json
import re
import socket
import ssl
import time
import urllib.error
import urllib.parse
import urllib.request

from collections import Counter
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timezone
from pathlib import Path
from statistics import mean, median


INPUT_FILE = Path("proxies.txt")
RESULTS_FILE = Path("proxy-results.csv")
SUMMARY_FILE = Path("proxy-summary.csv")

TEST_URL = "https://api.ipify.org?format=json"
DEFAULT_SCHEME = "http"

TIMEOUT_SECONDS = 8
ATTEMPTS = 3
MAX_WORKERS = 10

ALLOWED_SCHEMES = {"http", "https"}


def utc_now():
    return datetime.now(timezone.utc).isoformat()


def redact_text(value):
    text = str(value).replace("\n", " ").strip()
    text = re.sub(
        r"(://[^:@/\s]+:)[^@/\s]+@",
        r"\1***@",
        text,
    )
    return text[:180]


def safe_proxy_label(parsed):
    host = parsed.hostname or ""

    if ":" in host and not host.startswith("["):
        host = f"[{host}]"

    auth = ""

    if parsed.username:
        auth = f"{parsed.username}:***@"

    return (
        f"{parsed.scheme.lower()}://"
        f"{auth}{host}:{parsed.port}"
    )


def normalize_proxy(raw_value, line_number):
    value = raw_value.strip()

    if not value or value.startswith("#"):
        return None

    if "://" not in value:
        value = f"{DEFAULT_SCHEME}://{value}"

    parsed = urllib.parse.urlparse(value)
    scheme = parsed.scheme.lower()

    if scheme not in ALLOWED_SCHEMES:
        raise ValueError(
            f"Unsupported scheme: {parsed.scheme or 'missing'}"
        )

    if not parsed.hostname:
        raise ValueError("Missing proxy host")

    try:
        port = parsed.port
    except ValueError as exc:
        raise ValueError("Invalid proxy port") from exc

    if port is None:
        raise ValueError("Missing proxy port")

    username = parsed.username or ""
    password = parsed.password or ""

    credential_source = f"{username}:{password}"
    credential_id = hashlib.sha256(
        credential_source.encode("utf-8")
    ).hexdigest()[:12]

    dedupe_key = (
        f"{scheme}|{parsed.hostname.lower()}|"
        f"{port}|{credential_id}"
    )

    return {
        "source_line": line_number,
        "proxy_url": value,
        "safe_proxy": safe_proxy_label(parsed),
        "scheme": scheme,
        "host": parsed.hostname,
        "port": port,
        "dedupe_key": dedupe_key,
    }


def classify_error(exc):
    if isinstance(exc, urllib.error.HTTPError):
        if exc.code == 407:
            return "proxy_authentication_required_407"

        return f"http_error_{exc.code}"

    if isinstance(exc, urllib.error.URLError):
        reason = exc.reason

        if isinstance(reason, socket.timeout):
            return "timeout"

        if isinstance(reason, ssl.SSLError):
            return "tls_error"

        if isinstance(reason, ConnectionRefusedError):
            return "connection_refused"

        if isinstance(reason, socket.gaierror):
            return "dns_resolution_error"

        return f"url_error: {redact_text(reason)}"

    if isinstance(exc, (TimeoutError, socket.timeout)):
        return "timeout"

    return (
        f"{type(exc).__name__}: "
        f"{redact_text(exc)}"
    )


def test_proxy(item):
    proxy_handler = urllib.request.ProxyHandler({
        "http": item["proxy_url"],
        "https": item["proxy_url"],
    })

    opener = urllib.request.build_opener(proxy_handler)

    latencies = []
    exit_ips = []
    error_counts = Counter()

    for _ in range(ATTEMPTS):
        started = time.perf_counter()

        request = urllib.request.Request(
            TEST_URL,
            headers={
                "User-Agent": "ProxyListChecker/2.0",
                "Accept": "application/json",
            },
        )

        try:
            with opener.open(
                request,
                timeout=TIMEOUT_SECONDS,
            ) as response:
                body = response.read().decode("utf-8")
                data = json.loads(body)

                exit_ip = str(
                    data.get("ip", "")
                ).strip()

                if not exit_ip:
                    raise ValueError(
                        "Test endpoint returned no IP"
                    )

                latency_ms = round(
                    (
                        time.perf_counter()
                        - started
                    ) * 1000,
                    1,
                )

                latencies.append(latency_ms)
                exit_ips.append(exit_ip)

        except Exception as exc:
            error_counts[classify_error(exc)] += 1

    success_count = len(exit_ips)
    success_rate = round(
        (success_count / ATTEMPTS) * 100,
        1,
    )

    if success_count == ATTEMPTS:
        status = "live"
    elif success_count:
        status = "unstable"
    else:
        status = "failed"

    error_summary = "; ".join(
        f"{name} x{count}"
        for name, count
        in error_counts.most_common()
    )

    return {
        "source_line": item["source_line"],
        "duplicate_of_line": "",
        "proxy": item["safe_proxy"],
        "scheme": item["scheme"],
        "host": item["host"],
        "port": item["port"],
        "status": status,
        "attempts": ATTEMPTS,
        "success_count": success_count,
        "success_rate_percent": success_rate,
        "median_latency_ms": (
            round(median(latencies), 1)
            if latencies
            else ""
        ),
        "exit_ips": ";".join(
            sorted(set(exit_ips))
        ),
        "error": error_summary,
        "tested_at_utc": utc_now(),
    }


def result_template(
    source_line,
    proxy,
    status,
    error="",
    duplicate_of_line="",
):
    return {
        "source_line": source_line,
        "duplicate_of_line": duplicate_of_line,
        "proxy": proxy,
        "scheme": "",
        "host": "",
        "port": "",
        "status": status,
        "attempts": 0,
        "success_count": 0,
        "success_rate_percent": 0,
        "median_latency_ms": "",
        "exit_ips": "",
        "error": error,
        "tested_at_utc": utc_now(),
    }


def load_proxy_file(path):
    unique_items = []
    initial_results = []
    first_seen = {}

    source_rows = 0
    valid_rows = 0
    duplicate_rows = 0
    invalid_rows = 0

    with path.open(
        "r",
        encoding="utf-8",
    ) as file:
        for line_number, raw_value in enumerate(
            file,
            start=1,
        ):
            stripped = raw_value.strip()

            if not stripped or stripped.startswith("#"):
                continue

            source_rows += 1

            try:
                item = normalize_proxy(
                    raw_value,
                    line_number,
                )
            except Exception as exc:
                invalid_rows += 1

                initial_results.append(
                    result_template(
                        source_line=line_number,
                        proxy=redact_text(stripped),
                        status="invalid",
                        error=redact_text(exc),
                    )
                )
                continue

            if item is None:
                continue

            valid_rows += 1
            key = item["dedupe_key"]

            if key in first_seen:
                duplicate_rows += 1

                initial_results.append(
                    result_template(
                        source_line=line_number,
                        proxy=item["safe_proxy"],
                        status="duplicate",
                        error="Duplicate normalized entry",
                        duplicate_of_line=first_seen[key],
                    )
                )
                continue

            first_seen[key] = line_number
            unique_items.append(item)

    counts = {
        "source_rows": source_rows,
        "valid_rows": valid_rows,
        "invalid_rows": invalid_rows,
        "duplicate_rows": duplicate_rows,
        "unique_rows": len(unique_items),
    }

    return unique_items, initial_results, counts


def percentage(part, whole):
    if not whole:
        return 0.0

    return round((part / whole) * 100, 1)


def latency_quality_score(latency_ms):
    if latency_ms in {"", None}:
        return 0.0

    if latency_ms <= 500:
        return 100.0

    if latency_ms <= 1000:
        return 80.0

    if latency_ms <= 2000:
        return 60.0

    if latency_ms <= 5000:
        return 30.0

    return 10.0


def build_summary(results, counts):
    tested = [
        row for row in results
        if row["status"]
        in {"live", "unstable", "failed"}
    ]

    reachable = [
        row for row in tested
        if row["success_count"] > 0
    ]

    fully_live = [
        row for row in tested
        if row["status"] == "live"
    ]

    endpoint_latencies = [
        float(row["median_latency_ms"])
        for row in reachable
        if row["median_latency_ms"] != ""
    ]

    valid_format_rate = percentage(
        counts["valid_rows"],
        counts["source_rows"],
    )

    duplicate_rate = percentage(
        counts["duplicate_rows"],
        counts["valid_rows"],
    )

    uniqueness_rate = round(
        100 - duplicate_rate,
        1,
    )

    reachable_rate = percentage(
        len(reachable),
        len(tested),
    )

    fully_live_rate = percentage(
        len(fully_live),
        len(tested),
    )

    average_success_rate = round(
        mean(
            float(row["success_rate_percent"])
            for row in tested
        ),
        1,
    ) if tested else 0.0

    list_median_latency = round(
        median(endpoint_latencies),
        1,
    ) if endpoint_latencies else ""

    latency_score = latency_quality_score(
        list_median_latency
    )

    quality_score = round(
        (valid_format_rate * 0.15)
        + (uniqueness_rate * 0.10)
        + (reachable_rate * 0.35)
        + (average_success_rate * 0.25)
        + (latency_score * 0.15),
        1,
    )

    if quality_score >= 85:
        rating = "strong"
    elif quality_score >= 70:
        rating = "usable_with_review"
    elif quality_score >= 50:
        rating = "weak"
    else:
        rating = "poor"

    return {
        **counts,
        "reachable_rows": len(reachable),
        "fully_live_rows": len(fully_live),
        "valid_format_rate_percent": valid_format_rate,
        "duplicate_rate_percent": duplicate_rate,
        "reachable_rate_percent": reachable_rate,
        "fully_live_rate_percent": fully_live_rate,
        "average_success_rate_percent": (
            average_success_rate
        ),
        "list_median_latency_ms": (
            list_median_latency
        ),
        "latency_score": latency_score,
        "quality_score": quality_score,
        "rating": rating,
        "tested_at_utc": utc_now(),
    }


def write_results(path, rows):
    fieldnames = [
        "source_line",
        "duplicate_of_line",
        "proxy",
        "scheme",
        "host",
        "port",
        "status",
        "attempts",
        "success_count",
        "success_rate_percent",
        "median_latency_ms",
        "exit_ips",
        "error",
        "tested_at_utc",
    ]

    with path.open(
        "w",
        newline="",
        encoding="utf-8-sig",
    ) as file:
        writer = csv.DictWriter(
            file,
            fieldnames=fieldnames,
        )
        writer.writeheader()
        writer.writerows(rows)


def write_summary(path, summary):
    with path.open(
        "w",
        newline="",
        encoding="utf-8-sig",
    ) as file:
        writer = csv.DictWriter(
            file,
            fieldnames=list(summary.keys()),
        )
        writer.writeheader()
        writer.writerow(summary)


def main():
    if not INPUT_FILE.exists():
        raise FileNotFoundError(
            f"Input file not found: {INPUT_FILE}"
        )

    items, results, counts = load_proxy_file(
        INPUT_FILE
    )

    with ThreadPoolExecutor(
        max_workers=MAX_WORKERS
    ) as executor:
        future_map = {
            executor.submit(
                test_proxy,
                item,
            ): item["source_line"]
            for item in items
        }

        for future in as_completed(future_map):
            line_number = future_map[future]

            try:
                results.append(future.result())
            except Exception as exc:
                results.append(
                    result_template(
                        source_line=line_number,
                        proxy="",
                        status="failed",
                        error=(
                            "Unexpected worker error: "
                            f"{redact_text(exc)}"
                        ),
                    )
                )

    results.sort(
        key=lambda row: row["source_line"]
    )

    summary = build_summary(
        results,
        counts,
    )

    write_results(
        RESULTS_FILE,
        results,
    )

    write_summary(
        SUMMARY_FILE,
        summary,
    )

    print(f"Row results: {RESULTS_FILE}")
    print(f"List summary: {SUMMARY_FILE}")
    print(
        "Quality score: "
        f"{summary['quality_score']}/100"
    )
    print(f"Rating: {summary['rating']}")


if __name__ == "__main__":
    main()

The script uses Python’s standard library and does not require an extra package. It supports HTTP-style proxy URLs only. SOCKS4 and SOCKS5 entries require a SOCKS-aware library or a separate test tool and should not be mixed into this run.

The test endpoint returns the visible IP address. For large or recurring jobs, use a permitted test endpoint that can handle the expected request volume. The script is a starting template rather than a complete production monitoring system.

For implementation details, see the official Python documentation for ProxyHandler and ThreadPoolExecutor.

Export Results to CSV

The row-level CSV preserves failed, invalid, and duplicate entries so the report reflects the quality of the original list rather than only the surviving endpoints.

Table 2: Main fields exported by the proxy-list checker.
CSV Field Example Purpose
source_line 18 Links the result to the original file row
status live, unstable, failed, invalid, duplicate Separates network results from parsing and duplicate issues
success_rate_percent 66.7 Shows how many repeated attempts succeeded
median_latency_ms 428.5 Summarizes successful response times for the endpoint
exit_ips 198.51.100.24 Records unique IPs returned during the test
duplicate_of_line 7 Identifies the original row for a duplicate entry
error timeout x2 Groups repeated failures without exposing the password
tested_at_utc 2026-07-24T03:00:00+00:00 Shows when the result was produced

The summary CSV contains list-wide metrics such as valid-format rate, duplicate rate, reachable rate, average success rate, median latency, quality score, and rating.

Score the Whole Proxy List

The script uses an example 100-point scoring model. It is not a universal proxy-quality standard; adjust the weights and latency thresholds for the application using the list.

Table 3: Example weights used to calculate the proxy-list quality score.
Metric Weight What It Measures
Valid format rate 15% How much of the source file can be parsed correctly
Uniqueness rate 10% How much of the parsed list remains after duplicates are removed
Reachable rate 35% How many unique endpoints succeed at least once
Average success rate 25% How consistently the tested endpoints respond across repeated attempts
Latency score 15% How the median latency compares with the example thresholds
Example Quality Ratings
  • 85–100: Strong under the current test conditions
  • 70–84.9: Usable, but review unstable and failed rows
  • 50–69.9: Weak; substantial cleanup or replacement is needed
  • Below 50: Poor under the current test conditions

A score should always be read with the test URL, timeout, concurrency, attempt count, and timestamp. Two scores produced under different conditions are not directly comparable.

Public List Risk Without Overweighting It

Public proxy lists deserve a short risk check, but public-versus-managed comparisons should not take over a batch-validation article. Regardless of the source, the first task is to measure format quality, duplicate rate, reachability, stability, latency, and failure distribution.

Use unknown public endpoints only for low-risk experiments and parsing tests. Do not send private account data, payment information, customer records, or confidential traffic through proxies operated by unknown parties.

Public proxy list compared with managed proxy sources for reliability
Figure 2: Managed sources generally provide clearer authentication, replacement, and testing controls than unmanaged public lists.

Decide When to Retest or Remove Entries

A failed request should not always cause immediate deletion. Temporary routing problems, test-endpoint limits, and local network conditions can affect one run. Repeated failure patterns are more useful than a single result.

Table 4: Suggested actions for common proxy-list results.
Result Interpretation Recommended Action
Live across all attempts The endpoint was reachable and consistent during this run Keep it and schedule the next routine test
Unstable Only some attempts succeeded Retest with lower concurrency or another permitted endpoint
407 authentication error The proxy responded but rejected the credentials or access rule Verify credentials and authorization before removing it
Repeated timeout The endpoint did not respond within the configured limit Quarantine it and compare another test window
Invalid format The row cannot be tested without correction Edit or remove the source row
Duplicate The same normalized route and credentials appear earlier Remove it from the active list but preserve the duplicate count
Repeated failure across several runs The endpoint has a persistent failure history Remove it from the active list

Keep historical reports so you can compare live rate, latency, and failure distribution over time. A controlled list should have a defined retest schedule; fast-changing public lists generally need more frequent validation.

Proxy list failure patterns used for retesting and removal decisions
Figure 3: Use repeated failure patterns when deciding which entries to retest, quarantine, or remove.

Frequently Asked Questions

What is a proxy list checker?
A proxy list checker is a tool or script that reads multiple proxy entries, validates their format, removes duplicates, runs batch tests, and exports results such as status, success rate, latency, exit IP, errors, and test time.
How is checking a proxy list different from checking one proxy?
Checking one proxy answers whether that endpoint works in one environment. Checking a list also requires parsing, deduplication, controlled concurrency, repeated tests, reporting, scoring, and retesting rules.
What should a proxies.txt file look like?
Use one proxy per line, preferably with an explicit scheme such as http://host:port or http://username:password@host:port. URL-encode special characters in credentials and store the file securely.
Can this script test SOCKS proxies?
No. The standard-library example supports HTTP-style proxy URLs only. SOCKS4 and SOCKS5 testing requires a SOCKS-aware library or a separate tool.
Why does the script test each proxy more than once?
One successful request only proves that an endpoint responded once. Repeated attempts make it possible to calculate success rate and median latency and identify unstable proxies.
What is a good proxy-list score?
There is no universal score. The example model rates the list under one defined test setup. Use the same URL, timeout, concurrency, attempt count, and scoring weights when comparing different lists or test runs.
How often should I retest a proxy list?
Retest before important use and then on a schedule based on the source and workflow risk. Public or fast-changing lists may require frequent checks, while controlled lists can use a predictable maintenance window.
Why can a live proxy still fail on a specific website?
A successful batch test confirms that the proxy reached the selected test endpoint. Another website may use different network, browser, session, account, or policy checks.

Final Thoughts

A useful proxy list is the smaller, documented set that remains after format validation, deduplication, repeated testing, failure classification, scoring, and scheduled maintenance. Preserve the test conditions with every report, and keep single-proxy troubleshooting or website-specific detection checks outside the first list-validation pass.

About the author
View all articles
Ryan
Ryan
IP Proxy Research Team

Ryan is a web data and proxy infrastructure specialist focused on IP networks, scraping systems, SERP APIs, and global data access solutions. He shares practical insights on proxy usage, data collection architecture, and scalable web intelligence systems.

Service areas
Proxy IP Web Scraping & Data Infrastructure Specialist

You may be interested in

Proxy error diagram showing a failed connection through a proxy server

Proxy Error: What It Means and What to Check

A proxy error can stop a browser, API request, automation script, or desktop application from reaching its destination. The fastest way to diagnose it is to capture the exact error and identify whether the failure begins at the client, proxy endpoint, authentication layer, network path, or destination. Direct Answer A proxy error means a request failed somewhere along the path between the client, proxy server, and destination. Common causes include incorrect credentials, a closed port, protocol mismatch, blocked network traffic, local routing, DNS resolution outside the intended path, or a destination-side failure. Key Takeaways Read and record the exact error...

Ryan

Ryan

IP Proxy Research Team

Can Websites Detect Proxies?

Can Websites Detect Proxies?

Proxy detection is the process of estimating whether a request is passing through a proxy, VPN, hosting network, or another intermediary. A website may examine the visible IP address, network owner, IP reputation, DNS behavior, browser signals, cookies, traffic patterns, and account context. This article focuses on client-selected forward proxies. Reverse proxies operated by websites are part of a different server-side architecture and are not covered here. Direct Answer Yes, websites can sometimes detect proxy use. They normally infer it from several network, browser, session, and behavior signals rather than from a single label. A proxy can change the visible...

Ryan

Ryan

IP Proxy Research Team

Proxy anonymity check dashboard showing IP DNS and header tests

Proxy Anonymity Checker: How to Test IP, DNS, and Headers

An anonymity check is a practical review of what a website can observe when your browser or application connects through a proxy. It is not a universal score or a guarantee that every website will classify the connection in the same way. A useful check compares the visible IP, ASN, DNS behavior, WebRTC information, forwarding headers, reputation data, and browser context in the same environment where the proxy will actually run. This guide explains how to check proxy anonymity without relying on one result or changing several settings at once. The goal is to identify routing errors, unexpected network signals,...

Ryan

Ryan

IP Proxy Research Team

Ready to scale your data operations?
Join 10,000+ teams using IPWeb to power their web data collection. Start free today.

Strictly anti-abuse

Fraud, automated operation, and unauthorized use are prohibited.

Enterprise-level services

For legitimate commercial and technical use cases only

Risk control and restrictions

Abnormal behavior may trigger service restrictions or termination.

Compliance data use

Data acquisition and use must comply with relevant regulations.

Privacy protection first

The collection or misuse of sensitive personal information is strictly prohibited.

All services are subject to《the Usage Policy》