curl vs wget: I Benchmarked Both

– min read

wget silently restarted a broken download from zero. curl refused to resume it at all. Same server, same 120 MB file, two completely different failure modes. That disagreement is the whole reason I ran this benchmark.

Official projects: curl.se and curl on GitHub, GNU Wget. Both shipped on my Ubuntu box, nothing installed.

Server rack with hard drives and network cables in a data center


The setup

One python http.server on 127.0.0.1 serving a seeded, deterministic 120 MB file (60 repeats of a 2 MB block generated with random.Random(1234)), plus twenty 128 KB files and a small nested directory tree. All synthetic data, and no RFC-5737 addresses needed because everything stays on loopback. Every download is verified against the sha256 of the source. Timings are wall clock, 3 runs, median reported.

curl --version | head -1
# curl 8.18.0 (x86_64-pc-linux-gnu)
wget --version | head -1
# GNU Wget 1.25.0 built on linux-gnu.


Test 1: the plain download

Boring, and that is the point: both saturate loopback and the CPU inside python http.server. curl runs: 0.64 s, 0.54 s, 0.56 s. wget runs: 0.47 s, 0.55 s, 0.65 s. Medians below. Neither tool is meaningfully faster at moving bytes.

curl -fsS -o big.bin http://127.0.0.1:8099/big.bin
# median run: 0.56 s, 215 MB/s, sha256 match
wget -q -O big.bin http://127.0.0.1:8099/big.bin
# median run: 0.55 s, 220 MB/s, sha256 match


Test 2: twenty files, one command

Both tools take a URL list. wget did 20 files in 0.04 s, sequential curl -O in 0.07 s. The honest surprise: curl -Z (parallel transfers) took 1.3 s, several times slower on loopback. Connection setup overhead with no latency to hide. -Z earns its keep on WAN links, not LAN ones.

curl -sS -O http://127.0.0.1:8099/f00.bin -O http://127.0.0.1:8099/f01.bin  # ... 20 urls
# 20 files in 0.07 s
curl -sZ -O http://127.0.0.1:8099/f00.bin -O http://127.0.0.1:8099/f01.bin  # same 20 urls, -Z
# 20 files in 1.3 s (slower here)
wget -q http://127.0.0.1:8099/f00.bin http://127.0.0.1:8099/f01.bin  # ... 20 urls
# 20 files in 0.04 s


Test 3: resume a killed download

Both rate limited to 40M, killed after 1.2 s, then resumed. Against the plain python server, which ignores Range headers, the tools diverge hard: curl aborts with error 33 and leaves the file partial. wget shrugs and re-downloads the whole 120 MB in 2.92 s. Strict beats silent here: wget’s sha256 ends up correct, but you paid full bandwidth to get there.

# server WITHOUT Range support (python -m http.server)
curl -fsS --limit-rate 40M -C - -o big.bin http://127.0.0.1:8099/big.bin
# curl: (33) HTTP server doesn't seem to support byte ranges -> rc 33, file left at 62.9 MB
wget -q --limit-rate=40M -c -O big.bin http://127.0.0.1:8099/big.bin
# restarts from 0, 2.92 s, sha256 ok, 120 MB re-sent
# same tests against a Range-aware server:
# curl -C - resumed from 62.9 MB in 1.11 s, sha256 match
# wget -c resumed from 56.1 MB in 1.57 s, sha256 match

Debug note: the harness log filled with server-side BrokenPipeError tracebacks each time a download got killed. That was the kill working, not a bug. It also proved a point: curl notices truncation and fails loudly where wget papers over it.


Test 4: mirror a directory tree

wget -r -np -nH grabbed the nested tree in 0.02 s with one flag, though it also mirrored the autoindex index.html files, tidy those up later. curl has no recursive mode at all; a small parse-and-recurse script took 0.1 s. Fine at this scale, more code to own.

wget -q -r -np -nH http://127.0.0.1:8099/tree/
# got: tree/a.txt, tree/sub1/b.txt, tree/sub1/sub2/c.txt (+ index.html per dir)
# curl equivalent: loop over parsed index pages, recurse, -o each path
curl -fsS http://127.0.0.1:8099/tree/ | grep -o 'href="[^"]*"'
# 3 files in 0.1 s via the crawl script


The comparison

Bar chart comparing curl and wget median download throughput and batch file times
Testcurl 8.18.0wget 1.25.0
120 MB single file0.56 s (215 MB/s)0.55 s (220 MB/s)
20 x 128 KB files, one command0.07 s (-O sequential)0.04 s (multi-URL)
Resume, server supports Range1.11 s from 62.9 MB, sha256 match1.57 s from 56.1 MB, sha256 match
Resume, server ignores Rangefails with curl error 33, file left partialsilently restarts from 0, wastes bytes
Recursive mirror of a treeno flag, needs a crawl scriptone flag (-r -np -nH), 0.02 s
Auth, headers, POST, API workfirst class (-H, -u, -d, -w)basic, awkward
ProtocolsHTTP/1.1, HTTP/2, HTTP/3, FTP, SFTP and moreHTTP, HTTPS, FTP


Verdict

For scripts that hit APIs, auth, headers or status codes, curl wins, and it is not close: one flag each for -H, -u, -d and -w, plus Range strictness that fails loudly instead of quietly re-billing your bandwidth. Keep wget for mirroring: if you are pulling a directory tree or a wall of static files onto a box, -r -np is still one flag instead of twenty lines of index parsing you will rewrite every time.


If you want a web server to point these tools at, see the Caddy reverse proxy install, and for the log side of the same servers there is Nginx access log analysis with Bash and Python plus a TLS expiry checker built entirely on curl.

Mohamad ZulfahmyCybersecurity · OffSec · Entrepreneur
About me ↗
ON THIS PAGE
On this page