Skip to main content

Confirm

Are you sure?

Python · Code recipe

Run a Lighthouse audit in Python

Score any page for performance, accessibility, best practices and SEO, in Python 3.9+ with requests. Every program on this page runs as it stands — each one was run against a stub of the API before it was published.

By · Last updated: September 2026

TL;DR

To run a Lighthouse audit in Python, POST a URL and a device to https://urlpipe.dev/lighthouse and read the JSON with res.json(): four category scores from 0 to 1 and the lab metrics, LCP, CLS and TBT among them. An audit takes around 15 seconds, so the async variant with a webhook suits production. It costs 2 credits.

Free plan, no credit card. 1,000 credits a month.

One POST to /lighthouse runs a real Lighthouse audit in Chrome and answers with the four category scores — performance, accessibility, best practices, SEO — and the lab metrics behind the performance score. device picks mobile (the default, throttled CPU and network) or desktop.

In Python the report is read with res.json(). Scores run from 0 to 1, so the program multiplies by 100 to print what the Lighthouse report shows; metrics carry a ready-formatted displayValue. The guide to reading a Lighthouse audit explains what each number means.

Setup

Before you start

One dependency, requests. Put your API key in the environment so it never lands in the file.

Terminal
python3 -m pip install requests
export URLPIPE_API_KEY="your_api_key"

The request

Print the four scores, LCP, CLS and TBT

"sync": True holds the connection open until the result is ready and answers with it. Pass timeout= every time: requests has no default, and a page that never finishes loading would otherwise hang your process. Scores arrive from 0 to 1 and any of them can be None when Lighthouse could not compute it, so the loop checks before multiplying.

lighthouse_audit.py
import os

import requests

res = requests.post(
    "https://urlpipe.dev/lighthouse",
    headers={"Authorization": f"Bearer {os.environ['URLPIPE_API_KEY']}"},
    json={"url": "https://example.com", "device": "mobile", "sync": True},
    # requests waits forever unless told otherwise; a sync call can take up to 60 s.
    timeout=90,
)
res.raise_for_status()

report = res.json()
for name in ("performance", "accessibility", "best-practices", "seo"):
    score = (report["categories"].get(name) or {}).get("score")
    print(f"{name}: {'n/a' if score is None else round(score * 100)}")

metrics = {
    "LCP": "largest-contentful-paint",
    "CLS": "cumulative-layout-shift",
    "TBT": "total-blocking-time",
}
for label, key in metrics.items():
    print(f"{label}: {(report['metrics'].get(key) or {}).get('displayValue', 'n/a')}")

Run it: python3 lighthouse_audit.py

Given the example response on the docs page, it prints:

Output
performance: 95
accessibility: 88
best-practices: 92
seo: 90
LCP: 2.5 s
CLS: 0.05
TBT: 150 ms

Async

The async variant: a token, a webhook and a poll

Leave out sync and the answer is a token, straight away. for … else is the idiom for "ran out of attempts": the else runs only when the loop never hit break.

lighthouse_audit_async.py
import os
import time

import requests

API = "https://urlpipe.dev"
HEADERS = {"Authorization": f"Bearer {os.environ['URLPIPE_API_KEY']}"}

# No "sync": the request is accepted at once and the work carries on without you.
res = requests.post(
    f"{API}/lighthouse",
    headers=HEADERS,
    json={
        "url": "https://example.com",
        "device": "mobile",
        "report_to": "https://your-app.com/webhooks/urlpipe",
        "labels": {"customer": "acme"},
    },
    timeout=30,
)
res.raise_for_status()
token = res.json()["token"]
print(f"Accepted {token}")

# The result is POSTed to report_to when it is ready. Polling by token is the
# other way to collect it: no endpoint needed, and a backup for the webhook.
for _ in range(60):
    res = requests.get(f"{API}/result/{token}", headers=HEADERS, timeout=30)
    if res.status_code != 202:  # 202 means still processing
        break
    time.sleep(2)
else:
    raise SystemExit("Still processing after two minutes; try the token again later.")

if res.status_code == 422:
    raise SystemExit(f"The analysis failed: {res.json()['error']}")
if res.status_code == 410:
    raise SystemExit("The result is past the 30-day window; send the request again.")
res.raise_for_status()

report = res.json()
for name in ("performance", "accessibility", "best-practices", "seo"):
    score = (report["categories"].get(name) or {}).get("score")
    print(f"{name}: {'n/a' if score is None else round(score * 100)}")

metrics = {
    "LCP": "largest-contentful-paint",
    "CLS": "cumulative-layout-shift",
    "TBT": "total-blocking-time",
}
for label, key in metrics.items():
    print(f"{label}: {(report['metrics'].get(key) or {}).get('displayValue', 'n/a')}")

Run it: python3 lighthouse_audit_async.py

Errors

Handle errors and retries

raise_for_status() is fine for a script; a service wants to tell the failures apart. Check the status before calling res.json() — a 401 body is not JSON. sys.exit(message) prints to stderr and exits 1.

lighthouse_audit_errors.py
import os
import sys
import time

import requests

API_KEY = os.environ["URLPIPE_API_KEY"]


class URLpipeError(Exception):
    pass


def urlpipe(path, payload, attempts=5):
    """POST a sync request and return the response, or raise URLpipeError."""
    for attempt in range(attempts):
        res = requests.post(
            f"https://urlpipe.dev{path}",
            headers={"Authorization": f"Bearer {API_KEY}"},
            json={**payload, "sync": True},
            timeout=90,
        )
        if res.status_code == 200:
            return res
        if res.status_code == 401:
            raise URLpipeError("401: the API key is missing or wrong. Check URLPIPE_API_KEY.")

        try:
            body = res.json()
        except ValueError:
            body = {}
        code = body.get("error", "")
        detail = f"{code}: {body['message']}" if body.get("message") else code

        if res.status_code == 429 and code == "rate_limited":
            # Sending too fast: Retry-After says how long the window has left.
            time.sleep(int(res.headers.get("Retry-After", 1)))
        elif res.status_code == 429 and code == "concurrency_limit":
            # Every parallel slot on your plan is busy with your own requests.
            time.sleep(2**attempt)
        elif res.status_code == 504:
            # Still running on our side; the token collects it from GET /result/:token.
            raise URLpipeError(f"504 processing_timeout: collect it later with token {body['token']}")
        else:
            # 403 email_unverified, 422 (a bad parameter, or a page that would not load),
            # 429 quota_exceeded: sending the same request again gets the same answer.
            raise URLpipeError(f"{res.status_code}: {detail}")
    raise URLpipeError(f"429: still refused after {attempts} attempts")


try:
    res = urlpipe("/lighthouse", {"url": "https://example.com", "device": "mobile"})
except URLpipeError as error:
    sys.exit(f"URLpipe: {error}")
except requests.RequestException as error:
    sys.exit(f"Network error: {error}")

report = res.json()
for name in ("performance", "accessibility", "best-practices", "seo"):
    score = (report["categories"].get(name) or {}).get("score")
    print(f"{name}: {'n/a' if score is None else round(score * 100)}")

metrics = {
    "LCP": "largest-contentful-paint",
    "CLS": "cumulative-layout-shift",
    "TBT": "total-blocking-time",
}
for label, key in metrics.items():
    print(f"{label}: {(report['metrics'].get(key) or {}).get('displayValue', 'n/a')}")

Run it: python3 lighthouse_audit_errors.py

Details

What to know about /lighthouse

  • Lighthouse 13 reports four categories; the pwa key is always null, kept so the shape does not change.
  • The performance score weights TBT 30%, LCP 25%, CLS 25%, FCP 10% and Speed Index 10%.
  • INP needs real users and cannot be measured in a lab run; TBT is its lab stand-in.
  • "include_audits": "true" adds the full list of audits, with the elements to fix. Mobile and desktop are cached separately.
  • Want the audit run on a schedule, with a history and alerts? Full Stack Audit sells that as a monitored report; URLpipe is the primitive underneath.

Other languages

Run a Lighthouse audit in another language

More Python: every Python recipe

FAQ

Frequently asked questions

Mobile or desktop — which device should I audit?
Mobile, unless you know your traffic is desktop. It emulates a 360×640 phone with a 4× slower CPU and a slow 4G network, which gives the lower, more telling scores. Pass device: "desktop" for a 1350×940 window with no throttling.
Why isn't INP in the results?
INP is measured from real users interacting with the page, which a lab audit cannot do. Total Blocking Time is the lab metric that tracks it.
Why use the async variant for Lighthouse?
An audit takes around 15 seconds, longer on a heavy page. Async hands you a token at once and delivers the report to your webhook, so nothing holds a connection open waiting.
Do I need an SDK to call URLpipe from Python?
requests is the only dependency; there is no SDK to install, because the API is one POST per job.

Get a key and run it.

Free plan, no card. Paste your key into URLPIPE_API_KEY and every program on this page runs as it is.