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 (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.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)

Benchmarkv3.4.4v4.0.0Speedup
ada::url parse + href159.3 ns/URL128.8 ns/URL1.24× (−19%)
url_aggregator parse + href98.5 ns/URL81.5 ns/URL1.21× (−17%)
ada::can_parse58.5 ns/URL28.1 ns/URL2.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)

Benchmarkv3.4.4v4.0.0Speedup
ada::url164.3 ns/URL110.2 ns/URL1.49× (−33%)
url_aggregator98.2 ns/URL74.6 ns/URL1.32× (−24%)
ada::can_parse64.2 ns/URL39.9 ns/URL1.61× (−38%)

URLSearchParams and hosts

Benchmarkv3.4.4v4.0.0Speedup
url_search_params (Indeed fixtures)6.40 µs3.60 µs1.78× (−44%)
IPv4 non-decimal (ada::url)79.8 ns74.2 ns1.08× (−7%)
DNS-style hosts (ada::url)150.3 ns127.6 ns1.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_parse fast 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 to parse(...).has_value().
  • Absolute http/https parsing ( #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 by url and url_aggregator. Also a faster get_href for the common special-scheme case.
  • url_search_params form-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.4v4.0.0Δ
Amalgamated ada.cpp956 KiB706 KiB−26%
Amalgamated ada.h404 KiB415 KiB+3%
ada.h + ada.cpp zip253 KiB248 KiB−2%
Static libada.a782 KiB618 KiB−21%
Shared libada (stripped .dylib)616 KiB427 KiB−31%
ada.cpp.o __TEXT579 KiB393 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 : 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 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_host no 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

IssueSummary
#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)

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=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

Published by:

Yagiz Nizipli