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 (23 March 2026).
Since that release, main picked up 79 commits . 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.3 → libada.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
can_parsefast path ( #1106 , follow-ups #1111 , #1118 , #1119 , #1189 ): validation without constructing a full URL object, with size-limit and IDNA-expansion awareness so it stays equivalent toparse(...).has_value().- Absolute
http/httpsparsing ( #1175 ):try_parse_simple_absolute, a table-driven single-pass path for already-normalized absolute URLs (clean ASCII host, no credentials/port/IPv4/IDNA, no messy path segments), shared byurlandurl_aggregator. Also a fasterget_hreffor the common special-scheme case. url_search_paramsform-urlencoded decoding ( #1178 ): single-pass+/ percent decoding, reserved capacity, fewer intermediate strings.- Shared IPv4/IPv6 parsers ( #1179 ): one implementation for both URL types, hand-rolled number parsers, LUT-backed serialization, less duplication.
- ada-idna updates through v0.6.0: compressed tables, safer validation, and better throughput on IDNA-heavy hosts.
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.3 → libada.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 : restore exported symbols when needed, keep abidiff in CI
( #1099 ), 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 ):
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 allocatingThe 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:
set_hostname/set_hostno longer keep a partial host when parsing fails — full rollback ( #1169 ), fixing #1142 (set_hostname("sneaky.com#legit.org")and?variants).- Hex IPv4 pieces are bounded by value, not digit count ( #1185 ).
- No-scheme state no longer accepts arbitrary input that merely contains a fragment ( #1186 ).
- URLPattern / IDNA / file-scheme / Windows-drive and related edge cases listed under bug fixes below.
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 ). 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
- URLPattern tokenizer DoS on malformed UTF-8 ( #1125 ): malformed sequences always advance the cursor; strict policy errors, lenient policy emits an invalid-char token.
- Max input length ( #1126 ): defense in depth against huge or expansion-amplified URLs.
- Component offset wraparound ( #1123 ): unsigned wraparound eliminated when updating URL component offsets.
- IDNA Bidi rules ( #1136 , #1145 ): LTR labels must end with an allowed class; first-character Bidi rule enforced.
- Empty DNS length check UB ( #1109 ).
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 | set_hostname silently accepted # / ? and truncated the host — fixed by full rollback on host parse failure ( #1169 ). |
| #1127 | Unchecked simdutf result in IDNA to_unicode — addressed in subsequent ada-idna syncs. |
| #1098 | ABI breakage report from Debian packaging of 3.4.3→3.4.4 — led to restoring set_scheme export and adding ABI CI ( #1099 ). |
Spec / correctness fixes (selected)
- #1186 — no-scheme + fragment false accepts
- #1185 — hex IPv4 overflow by digit count
- #1181 —
set_hostdash-dot / port path - #1167 — hostname canonicalize fast path skipped IPv4/IDNA
- #1166 — explicit port
0preserved acrossset_protocolon non-special schemes - #1168 — leading-zero ports in canonicalize
- #1157 — empty host on non-special URL without authority
- #1156 — Windows drive letter must be exactly two code points
- #1139 —
set_protocolslow-path return consistency - URLPattern: #1187 , #1188 , #1190 , #1191 , #1171 , #1172 , #1148
Web Platform Tests were rolled forward throughout the cycle. We also listed an official Kotlin client alongside the other language bindings ( #1102 ).
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=AdaURLSingle-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