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.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 (opens in a new tab) , follow-ups #1111 (opens in a new tab) , #1118 (opens in a new tab) , #1119 (opens in a new tab) , #1189 (opens in a new tab) ): 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 (opens in a new tab) ):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 (opens in a new tab) ): single-pass+/ percent decoding, reserved capacity, fewer intermediate strings.- Shared IPv4/IPv6 parsers ( #1179 (opens in a new tab) ): 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 (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:
set_hostname/set_hostno longer keep a partial host when parsing fails — full rollback ( #1169 (opens in a new tab) ), fixing #1142 (opens in a new tab) (set_hostname("sneaky.com#legit.org")and?variants).- Hex IPv4 pieces are bounded by value, not digit count ( #1185 (opens in a new tab) ).
- No-scheme state no longer accepts arbitrary input that merely contains a fragment ( #1186 (opens in a new tab) ).
- 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 (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
- URLPattern tokenizer DoS on malformed UTF-8 ( #1125 (opens in a new tab) ): malformed sequences always advance the cursor; strict policy errors, lenient policy emits an invalid-char token.
- Max input length ( #1126 (opens in a new tab) ): defense in depth against huge or expansion-amplified URLs.
- Component offset wraparound ( #1123 (opens in a new tab) ): unsigned wraparound eliminated when updating URL component offsets.
- IDNA Bidi rules ( #1136 (opens in a new tab) , #1145 (opens in a new tab) ): LTR labels must end with an allowed class; first-character Bidi rule enforced.
- Empty DNS length check UB ( #1109 (opens in a new tab) ).
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)
- #1186 (opens in a new tab) — no-scheme + fragment false accepts
- #1185 (opens in a new tab) — hex IPv4 overflow by digit count
- #1181 (opens in a new tab) —
set_hostdash-dot / port path - #1167 (opens in a new tab) — hostname canonicalize fast path skipped IPv4/IDNA
- #1166 (opens in a new tab) — explicit port
0preserved acrossset_protocolon non-special schemes - #1168 (opens in a new tab) — leading-zero ports in canonicalize
- #1157 (opens in a new tab) — empty host on non-special URL without authority
- #1156 (opens in a new tab) — Windows drive letter must be exactly two code points
- #1139 (opens in a new tab) —
set_protocolslow-path return consistency - URLPattern: #1187 (opens in a new tab) , #1188 (opens in a new tab) , #1190 (opens in a new tab) , #1191 (opens in a new tab) , #1171 (opens in a new tab) , #1172 (opens in a new tab) , #1148 (opens in a new tab)
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)