Run httplint against the Common Crawl WAT archives and report aggregate statistics on the HTTP-level issues found in real-world responses.
There are two ways to run it, sharing the same linting and reporting code — only the input distribution and result aggregation differ:
- Local — lint a handful of WAT files fetched over HTTP. No AWS required; good for spot checks and development. Start here.
- Amazon EMR — distribute an entire Common Crawl release across a transient EMR cluster for a full-crawl analysis (~123M responses). Needs an AWS account and costs money per run.
Lint a few WAT files with no AWS setup. Requires Python 3.10+; the virtualenv is created automatically on first run.
# One or more WAT paths, one per line:
echo crawl-data/CC-MAIN-2024-18/segments/.../warc/CC-MAIN-...warc.gz > paths.txt
make report.html # fetches the WATs, lints, writes report.html + report.mdmake report.html wraps the cc-lint lint CLI: it fetches each WAT over
HTTP from data.commoncrawl.org, runs httplint over the response
metadata, and renders the report. See cc-lint lint --help for the full
option set.
The EMR path scales the same linting pipeline across an entire
Common Crawl release. It uses mrjob to launch a transient EMR
cluster, distributes WARC paths across mappers, runs the linter in
fork-isolated child processes (so a single bad WARC cannot take a
mapper down), and aggregates the per-mapper StatsCollector dicts
into one summary record.
EMR jobs cost money on your AWS account. Always confirm the cluster terminated in the EMR console after a run.
-
Install and configure the AWS CLI (region
us-east-1is closest to Common Crawl's S3 bucket). -
Create your config files.
make setupprompts for your S3 bucket name and generates all three from the tracked*.exampletemplates:make setup
It writes
cc-lint.mk(yourOUTPUT_DIR/PATHS_PREFIX/WHEEL_S3_PATH) plusmrjob.confandmrjob-test.conf(thecloud_log_dirwhere mrjob preserves EMR cluster logs, somake emr-timing EMR_LOG_CLUSTER_ID=j-...has something to download for postmortems). All three are gitignored, so your bucket never lands in a commit.make setuprefuses to overwrite an existing file — delete it first to regenerate.To do it by hand instead,
cpeach*.exampleto its real name and replaceYOUR-BUCKET. Every Make target readscc-lint.defaults.mkfirst, then yourcc-lint.mkoverrides (or anyCONFIG=/path/to/another.mk). -
Build and upload the dependency wheel bundle. The bootstrap installs packages from
/tmp/wheelson each EMR node with--no-index, so no PyPI traffic happens during a job:make wheels # builds wheels in ./wheels via amazonlinux:2023 docker make upload-wheels # sync ./wheels to $(WHEEL_S3_PATH)
-
Cache the Tranco top-sites CSV locally — it is uploaded with each job via
--files:make tranco-cache
-
(Optional) Build the IP-to-ASN table for infrastructure fingerprinting. Every report fingerprints the CDN / server / framework behind each response from signal headers; supplying a CAIDA pfx2as snapshot additionally resolves the crawl-time
WARC-IP-Addressto an ASN, which catches CDNs that strip identifying headers. SetIPASN_V4_URL(and optionallyIPASN_V6_URL) in your config to a snapshot near the crawl month, then:make ipasn-cache
Once built, the table is shipped to mappers (and used by the local lint) automatically. Without it, fingerprinting is header-only.
make test-emr runs the full pipeline against
tests/fixtures/warc.paths.txt with a small instance fleet
(mrjob-test.conf) and LIMIT=1 so only a single WARC is processed.
Use it to validate AWS plumbing, wheel availability, and bootstrap
correctness before a full run:
make test-emrSuccessful runs land in results/test-<RUN_ID>/ with part-*
records from EMR plus the rendered report.html and report.md.
make emrThis pipeline does, in order:
cc_lint.emr.split_pathsreadss3://commoncrawl/crawl-data/$(CRAWL_ID)/warc.paths.gz(requester pays handled automatically) and uploadsMAP_TASKSchunk files to$(PATHS_PREFIX)$(CRAWL_ID)-$(RUN_ID)/.cc_lint.emr.jobruns on EMR withmrjob.confandREDUCESreducers. Each mapper forks a child per WARC path, pickles aStatsCollectorsnapshot back, and merges into a single per-mapper dict.aws s3 syncpulls the reducer output intoresults/$(CRAWL_ID)-$(RUN_ID)/.cc_lint.emr.finalizemerges the sharded reducer records from thepart-*files —globals, per-notenote:*, and the per-dimension shards (csp_sizes,vary,cache_control,value_histograms,cooccur,note_cooccur,transition) — and rendersreport.htmlreport.md.
If you already have a results/<run-name>/ directory with part-*
files synced from S3, regenerate the report without rerunning EMR:
make report RESULTS_DIR=results/CC-MAIN-2026-12-20260520-101500
# or:
make results/CC-MAIN-2026-12-20260520-101500/report.htmlIf you preserved EMR logs (MRJOB_CLEANUP=NONE or via the EMR
console), pass the cluster id to surface per-WARC timings and
failures:
make emr-timing EMR_LOG_CLUSTER_ID=j-XXXXXXXXThis downloads stderr.gz from S3, parses the structured
INFO: finished WARC ... lines emitted by the mapper, and prints a
Markdown summary of total/process/iterator times, top-N slow WARCs,
and any child-process failures.
make show-config prints the effective values. Important knobs:
| Variable | Purpose |
|---|---|
CRAWL_ID |
Common Crawl release to process (e.g. CC-MAIN-2026-12) |
TOP_N, LOCAL_TOP_N |
Tranco top-N filter for full / local runs |
SAMPLE_TOP_N |
Tranco ceiling for collecting per-note sample URLs |
RECORD_LIMIT |
Max records per WARC (0 = all) |
MAP_TASKS, REDUCES |
Full-run cluster sizing |
TEST_MAP_TASKS, TEST_REDUCES, LIMIT |
Smoke-test sizing |
OUTPUT_DIR, PATHS_PREFIX, WHEEL_S3_PATH |
S3 locations |
MRJOB_CLEANUP |
Set to NONE to keep cluster + logs for postmortem |
Development setup, the test / typecheck / lint workflow, commit conventions, and a tour of the project layout live in CONTRIBUTING.md.