Markdown pipelines
The markdown export and refetch commands: schemas, HuggingFace layout, resume behaviour, and the three knobs that set throughput.
The markdown group turns Common Crawl WARC files into Markdown Parquet datasets and commits them to a HuggingFace dataset repo.
There are two pipelines and they answer different questions.
| Command | Source of the HTML | Use it when |
|---|---|---|
markdown export |
the HTML Common Crawl already captured | you want the crawl as it was, no live traffic |
markdown refetch |
a live fetch of every URL in the shard | you want today's page plus response metadata |
Both walk the same unit of work: one WARC file from a crawl's manifest is one shard, one shard becomes one Parquet file, and one Parquet file lands at a fixed path in the repo.
ccrawl markdown export --shards 0 --repo open-index/open-markdown-v2
ccrawl markdown refetch --shards 0 --repo open-index/open-markdown-refetch-v1
Set HF_TOKEN before either command, or pass --push=false to write Parquet locally and skip HuggingFace entirely.
Selecting shards
--shards takes a single index, an inclusive range, a comma list, a mix of the two, or all.
Indices are 0-based positions in the crawl's warc.paths.gz manifest, which has around 90 000 entries per crawl.
ccrawl markdown export --shards 0
ccrawl markdown export --shards 0-49
ccrawl markdown export --shards 1,3,5
ccrawl markdown export --shards 0-9,20
ccrawl markdown export --shards all --limit 200
An out-of-range index is a usage error, not a silent skip.
--limit trims the resolved list after expansion, so --shards all --limit 200 means the first 200 shards of the manifest.
Output schemas
open-markdown-v2, written by markdown export
| Column | Type | Meaning |
|---|---|---|
doc_id |
string | stable SHA-256 of the URL, first 16 bytes as hex |
url |
string | the URL Common Crawl captured |
host |
string | hostname taken from the URL |
crawl_date |
string | WARC-Date of the record, YYYY-MM-DD |
warc_record_id |
string | WARC-Record-ID of the source record |
html_length |
int | raw HTML body bytes before conversion |
markdown_length |
int | converted Markdown bytes |
markdown |
string | the converted Markdown |
open-markdown-refetch-v1, written by markdown refetch
Everything the export schema has, plus the live response and its timings.
doc_id is computed the same way, so the two datasets join on it directly.
| Column | Type | Meaning |
|---|---|---|
doc_id |
string | stable SHA-256 of the URL, first 16 bytes as hex |
url |
string | the URL taken from the Common Crawl shard |
final_url |
string | URL after redirects |
host |
string | hostname |
ip_address |
string | server IP the fetch landed on |
crawl_id |
string | the CC crawl the URL came from, for example CC-MAIN-2026-25 |
crawl_date |
string | WARC-Date of the original record |
warc_record_id |
string | WARC-Record-ID of the original record |
fetched_at |
int64 | Unix milliseconds of the live fetch |
status |
int | HTTP status code |
content_type |
string | Content-Type response header |
fetch_duration_ms |
int | whole fetch wall clock |
ttfb_ms |
int | time to first byte |
etag |
string | ETag response header |
last_modified |
string | Last-Modified response header |
resp_headers |
string | full response head, status line and headers |
body_length |
int | raw body bytes |
digest |
string | SHA-1 hex of the raw body |
html_length |
int | HTML body bytes, set only when the response is HTML |
markdown_length |
int | converted Markdown bytes |
markdown |
string | the converted Markdown |
error |
string | fetch error, empty on success |
A failed fetch still produces a row.
That is deliberate: a shard of Common Crawl URLs contains a lot of dead hosts, and knowing a URL is gone is a result worth keeping.
Filter on error = '' when you only want live pages.
HuggingFace path layout
Both pipelines write the same layout, so the two repos load with the same code.
data/crawl=CC-MAIN-2026-25/000000.parquet
data/crawl=CC-MAIN-2026-25/000001.parquet
data/crawl=CC-MAIN-2026-25/000042.parquet
The number is the shard index, zero padded to six digits, so a file name maps straight back to a manifest position.
The crawl= component is a Hive partition, which means DuckDB and the datasets library both see crawl as a column without any extra configuration.
SELECT host, count(*)
FROM read_parquet('data/crawl=*/*.parquet', hive_partitioning=1)
WHERE crawl = 'CC-MAIN-2026-25'
GROUP BY host ORDER BY 2 DESC LIMIT 20;
Parquet is written with zstd compression.
Resuming
Every committed shard is appended to a ledger file, <out>/.committed unless --ledger moves it.
On start the run reads the ledger and skips anything already in it, then reports what it found:
markdown: ledger /Users/you/data/ccrawl/markdown/CC-MAIN-2026-25/.committed already records 412 committed shards
So a killed run resumes by re-running the exact same command. Nothing is recomputed and nothing is committed twice.
Local Parquet files are deleted after they commit, which is what keeps a --shards all run inside a fixed disk budget.
Pass --keep-parquet to hold on to them, and watch the disk if you do.
--min-free-gb (default 2) pauses new downloads when free space drops below the threshold rather than failing mid-shard.
A per-shard conversion failure is logged and counted, and the run keeps going. Only a fatal commit failure or a cancelled context aborts, and the final line always reports the split:
markdown: 48 committed, 2 skipped, 0 failed of 50 | 1843229 rows | html=61.2 GiB md=8.4 GiB parquet=2.1 GiB | 41m18s elapsed (72.6 shards/hour)
Exit codes
The markdown pipelines resume from the ledger, so they do not use the supervised-restart exit code. They exit 0 on success and 1 on a fatal error, and you re-run the same command to continue.
urls publish and domains publish are the commands that exit 75, because those runs commit into a single growing dataset and have a stall clock.
See exit codes for the full contract.
Tuning: parallel, workers, commit-batch
Three flags set throughput, and they control three different resources.
| Flag | Resource | Default (export) | Default (refetch) |
|---|---|---|---|
--parallel |
network and disk, shards in flight | 3 | 2 |
--workers |
CPU, HTML to Markdown conversion | NumCPU |
NumCPU |
--commit-batch |
HuggingFace round trips per commit | 1 | 1 |
--parallel is how many shards download and process at once.
Raise it when the download is the bottleneck, which it usually is on a fast machine with a slow link.
Each in-flight shard holds a WARC on disk, so --parallel 8 on 1 GiB shards means roughly 8 GiB of working set.
--workers is a single conversion pool shared across every in-flight shard, not a per-shard pool.
That is the point: raising --parallel does not oversubscribe the cores.
Leave it at 0 to get NumCPU unless you are sharing the box.
--commit-batch is how many finished Parquet files go into one HuggingFace commit.
The commit round trip is slow and the HF API takes one commit at a time per repo, so committing one file at a time throttles a fast run.
A background committer batches finished shards and commits them off the critical path, which means a larger batch trades commit frequency for throughput.
Use 1 for a short run where you want each shard durable immediately, 10 or more for a long --shards all run.
A reasonable starting point on a machine with a fast link:
ccrawl markdown export --shards all --parallel 4 --commit-batch 10
Refetch has a fourth knob
markdown refetch also drives a live fetch, so it adds --fetch-workers, the number of concurrent requests per shard.
Left at 0 it derives a value from the process file descriptor limit divided across --parallel, capped at 3000, and prints what it picked:
refetch: fd limit 1048576
refetch: auto fetch-workers=3000 (per shard, 2 shards in parallel)
The command raises its own soft RLIMIT_NOFILE at startup, so you do not need a shell ulimit change.
--rate sets a per-host request rate in requests per second, 0 meaning unlimited, and --max-redirects caps redirect hops at 5 by default.
--fetch-only stores the raw HTML and skips conversion, so the fetch runs at full speed and you convert offline later over the html column.
That is the right shape when you are fetch-bound and want the network phase to finish as fast as the hosts allow.
Downloaded WARC shards are cached under <data-dir>/ami/warc so a re-run skips the download.
Move it with --warc-cache-dir or turn it off with --no-warc-cache.
Refetch reports where the time went, which is the fastest way to find out what to tune:
phase totals: extract=61s fetch=1840s convert=402s export=88s publish=120s
phase avg/shard: extract=30s fetch=920s convert=201s export=44s | fetch-only 431 pages/s (urls/fetch-sec)
failures: 184203 total | dns=91002 timeout=64881 refused=19422 skip=1102 other=7796
Flags
Shared by markdown export and markdown refetch:
| Flag | Meaning |
|---|---|
--shards |
Shard range: N, N-M, N,M, or all |
--repo |
HuggingFace dataset repo, org/name |
--out |
Directory for Parquet files |
--push |
Commit each shard to HuggingFace (default true, --push=false to stay local) |
--limit |
Process at most this many shards (0 = all) |
--parallel |
Shards in flight at once |
--workers |
Conversion workers shared across shards (0 = NumCPU) |
--commit-batch |
Parquet files per HuggingFace commit |
--keep-parquet |
Keep local Parquet after it commits |
--min-free-gb |
Pause new downloads below this much free disk |
--ledger |
Resume ledger path (default <out>/.committed) |
markdown export only:
| Flag | Meaning |
|---|---|
--skip-errors |
Continue past per-shard failures instead of aborting |
markdown refetch only:
| Flag | Meaning |
|---|---|
--fetch-workers |
Concurrent fetches per shard (0 = auto from the fd limit) |
--fetch-only |
Store raw HTML and skip conversion |
--rate |
Per-host request rate limit in req/s (0 = unlimited) |
--max-redirects |
Maximum redirect hops per fetch (default 5) |
--warc-cache-dir |
Where to cache downloaded WARC shards |
--no-warc-cache |
Do not cache downloaded shards |
For a worked run, see the Markdown corpus guide.