Skip to main content
Yagiz
7 min read Performance

Announcing Ada v4: Validating 35.6M URLs per second

Ada URL Parser is available on github.com/ada-url/ada
Announcing Ada v4: Validating 35.6M URLs per second

Ada is the fastest WHATWG-compliant URL parser in the world. It powers URL handling in Node.js, Cloudflare Workers, Redpanda, Kong, Telegram, Datadog, ClickHouse, and more. We are announcing Ada v4.0.0 — the first major release since v3.4.4 (opens in a new tab) (23 March 2026).

Since that release, main picked up 79 commits (opens in a new tab) . This release is about three things: speed on the common path, a materially smaller IDNA/binary footprint, and hardening against incorrect and unsafe inputs. Shared-library consumers should plan for the soname bump: libada.so.3libada.so.4.

All numbers below were measured on an Apple M5 Max (macOS 26.5), Release builds (-DCMAKE_BUILD_TYPE=Release -DADA_BENCHMARKS=ON), five Google Benchmark repetitions (means). Same machine, same flags, same datasets for v3.4.4 and current main (16a57723).

Performance

Large realistic dataset (benchdata, ~100k URLs)

Benchmark v3.4.4 v4.0.0 Speedup
ada::url parse + href 159.3 ns/URL 128.8 ns/URL 1.24× (−19%)
url_aggregator parse + href 98.5 ns/URL 81.5 ns/URL 1.21× (−17%)
ada::can_parse 58.5 ns/URL 28.1 ns/URL 2.08× (−52%)

Throughput on this corpus is about 6.3M → 7.8M URLs/s for ada::url, 10.2M → 12.3M URLs/s for url_aggregator, and 17.1M → 35.6M URLs/s for can_parse.

Small “top sites” set (bench)

Benchmark v3.4.4 v4.0.0 Speedup
ada::url 164.3 ns/URL 110.2 ns/URL 1.49× (−33%)
url_aggregator 98.2 ns/URL 74.6 ns/URL 1.32× (−24%)
ada::can_parse 64.2 ns/URL 39.9 ns/URL 1.61× (−38%)

URLSearchParams and hosts

Benchmark v3.4.4 v4.0.0 Speedup
url_search_params (Indeed fixtures) 6.40 µs 3.60 µs 1.78× (−44%)
IPv4 non-decimal (ada::url) 79.8 ns 74.2 ns 1.08× (−7%)
DNS-style hosts (ada::url) 150.3 ns 127.6 ns 1.18× (−15%)

The already-tuned pure-decimal IPv4 fast path is roughly unchanged after the shared IP parser refactor. The wins show up on the general IPv4 path, host parsing overall, search-params decoding, absolute http(s) parsing, and especially can_parse.

What got faster

Bundle size

IDNA table compression is the main story: src/ada_idna.cpp dropped from 683 KiB → 400 KiB (−41%). That flows through to every packaging form we ship.

Artifact (Release, Apple Clang) v3.4.4 v4.0.0 Δ
Amalgamated ada.cpp 956 KiB 706 KiB −26%
Amalgamated ada.h 404 KiB 415 KiB +3%
ada.h + ada.cpp zip 253 KiB 248 KiB −2%
Static libada.a 782 KiB 618 KiB −21%
Shared libada (stripped .dylib) 616 KiB 427 KiB −31%
ada.cpp.o __TEXT 579 KiB 393 KiB −32%

Node.js and other single-header consumers get a noticeably smaller drop-in. Distro shared libraries get a matching soname bump and a smaller .so/.dylib.

Breaking changes

Shared library soname: libada.so.3libada.so.4

ADA_LIB_SOVERSION moved from 3 to 4. Downstream packages that link the shared library must rebuild against 4.0.0 (or install a package that provides libada.so.4). Static linking and the amalgamated ada.h / ada.cpp pair are unaffected beyond the usual recompile.

This follows the ABI discipline we added after Debian packaging feedback on v3.4.4 (opens in a new tab) : restore exported symbols when needed, keep abidiff in CI ( #1099 (opens in a new tab) ), and bump the soname when the ABI intentionally changes.

Configurable maximum URL length

Parsing and setters now enforce a configurable maximum on both the raw input and the normalized href, including percent-encoding expansion ( #1126 (opens in a new tab) ):

ada::set_max_input_length(2048);           // bytes
auto url = ada::parse("http://example.com/" + std::string(2048, 'a'));
assert(!url);                              // normalized form too long
 
uint32_t limit = ada::get_max_input_length();
size_t n = url->get_href_size();           // length without allocating

The default remains ~4 GB (UINT32_MAX), so most applications see no behavior change. If you embed Ada in a service that accepts untrusted URL strings, set a tighter limit. C API: ada_set_max_input_length / ada_get_max_input_length.

Correctness fixes that reject previously accepted bad input

Several fixes change results for inputs that should never have succeeded. If you were accidentally relying on the old behavior, you will see parse/setter failures instead of silently wrong URLs:

C API lifetime documentation

ada_string values from getters are borrowed views into the ada_url instance. They are invalidated by any mutating ada_set_* / ada_clear_* call ( #1091 (opens in a new tab) ). This was always true; 4.0.0 documents it explicitly. Prefer ada_owned_string getters (e.g. ada_get_origin) when you need a stable copy.

Security and hardening

Fuzzing coverage also grew (serializers harness, stronger invariants, C API harness moved onto the C++ amalgamation).

Bug reports addressed since v3.4.4

Issues

Issue Summary
#1142 (opens in a new tab) set_hostname silently accepted # / ? and truncated the host — fixed by full rollback on host parse failure ( #1169 (opens in a new tab) ).
#1127 (opens in a new tab) Unchecked simdutf result in IDNA to_unicode — addressed in subsequent ada-idna syncs.
#1098 (opens in a new tab) ABI breakage report from Debian packaging of 3.4.3→3.4.4 — led to restoring set_scheme export and adding ABI CI ( #1099 (opens in a new tab) ).

Spec / correctness fixes (selected)

Web Platform Tests were rolled forward throughout the cycle. We also listed an official Kotlin client alongside the other language bindings ( #1102 (opens in a new tab) ).

Try it

git clone https://github.com/ada-url/ada
cd ada
cmake -B build -DADA_BENCHMARKS=ON -DCMAKE_BUILD_TYPE=Release
cmake --build build
./build/benchmarks/benchdata --benchmark_filter=AdaURL

Single-header consumers can amalgamate (python3 singleheader/amalgamate.py) or grab the GitHub release assets (ada.h / ada.cpp / singleheader.zip).

If you ship Ada as a shared library, plan for libada.so.4. If you accept untrusted URL strings, set ada::set_max_input_length to something appropriate for your service.

Thanks to everyone who filed issues, sent fuzz crashes, and opened PRs — especially Debian packaging for the ABI report that sharpened our export/CI story.

Full changelog: v3.4.4…v4.0.0 (opens in a new tab)