<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Frank DENIS random thoughts.</title>
  <link href="https://00f.net/atom.xml" rel="self"/>
  <link href="https://00f.net"/>
  <updated>2026-07-08T15:26:43+02:00</updated>
  <id>https://00f.net</id>
  
  <author>
    <name>Frank Denis (Jedi/Sector One)</name>
    <email>blog at pureftpd.org</email>
  </author>
  
  
  <entry>
    <title>The best WebAssembly runtime may still be no runtime at all</title>
    <link href="https://00f.net/2026/07/08/webassembly-compilation-to-c-2026/"/>
   <updated>2026-07-08T00:00:00+02:00</updated>
   <id>https://00f.net/2026/07/08/webassembly-compilation-to-c-2026</id>
   <content type="html">&lt;p&gt;In 2023, I wrote that &lt;a href=&quot;/2023/12/11/webassembly-compilation-to-c/&quot;&gt;the best WebAssembly runtime may be no runtime at all&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To summarize: if you already have a WebAssembly module, translating it to C and compiling that C with a normal native compiler can be surprisingly hard to beat.&lt;/p&gt;

&lt;p&gt;I expected that argument to get weaker over time, because WebAssembly runtimes keep improving. Their compilers have better register allocators, better lowering, better support for newer WebAssembly instructions, and more deployment polish than a small transpiler can reasonably have.&lt;/p&gt;

&lt;p&gt;Then I ran the 2026 benchmark.&lt;/p&gt;

&lt;p&gt;The 2026 &lt;a href=&quot;/2026/06/23/webassembly-runtimes-2026/&quot;&gt;WebAssembly benchmark&lt;/a&gt; has the full table for other runtimes.&lt;/p&gt;

&lt;p&gt;And the WebAssembly-to-C path performed very well, but was beaten by new runtimes implementing the “wide arithmetic” WebAssembly proposal.&lt;/p&gt;

&lt;p&gt;I was curious to see how the WebAssembly-to-C approach would work with if that proposal was implemented.&lt;/p&gt;

&lt;p&gt;So, let’s compare:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmer 7.1.0&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmtime 46.0.0&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;WABT &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wasm2c&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wasm2c&lt;/code&gt;, I added support for wide arithmetic, which was surprisingly trivial to implement. You can find my fork &lt;a href=&quot;https://github.com/dip-proto/wabt&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The generated C code simply uses compiler carry intrinsics and C’s 128-bit integer type.&lt;/p&gt;

&lt;p&gt;Note that I compiled the generated C with WABT’s Segue memory mode enabled, with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;zig cc -O3 -march=native&lt;/code&gt;, on the exact same libsodium benchmark suite as the previous runtime post.&lt;/p&gt;

&lt;p&gt;Every build uses the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lime1+simd128+wide_arithmetic&lt;/code&gt; feature set.&lt;/p&gt;

&lt;p&gt;Oh, and before you ask: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wasm2c&lt;/code&gt; supports memory protection using guard pages, like other runtimes.&lt;/p&gt;

&lt;h2 id=&quot;the-speed-numbers&quot;&gt;The speed numbers&lt;/h2&gt;

&lt;p&gt;The numbers below are slowdowns relative to the native libsodium build. A value of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.25x native&lt;/code&gt; means the benchmark took 25% more time than native code.&lt;/p&gt;

&lt;p&gt;Lower is better.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/wasm2c2026/speed.png&quot; alt=&quot;wasm2c with wide arithmetic, ranked by geomean slowdown&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The median is a tie with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmer&lt;/code&gt;. But the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wasm2c&lt;/code&gt; executable used 0.887x as much time as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmer&lt;/code&gt; on the geomean, and 0.814x as much as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmtime&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The dumb C path survived the feature upgrade. Once the new arithmetic instructions were added, a normal C compiler could again produce code competitive with dedicated WebAssembly compilers.&lt;/p&gt;

&lt;h2 id=&quot;memory-usage&quot;&gt;Memory usage&lt;/h2&gt;

&lt;p&gt;I also measured peak resident set size with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/usr/bin/time -v&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is cold process RSS for one command invocation, using the median of fifteen runs. It includes the runtime executable, its startup state, JIT or compilation machinery used by that command, and anything else the process maps while running the benchmark.&lt;/p&gt;

&lt;p&gt;Note that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmer&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmtime&lt;/code&gt; have a fixed cost that can be amortized if a service keeps the runtime alive and runs many modules or many calls.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/wasm2c2026/memory.png&quot; alt=&quot;Cold process memory, median of 15 runs&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Most of that difference is fixed engine overhead. The module itself costs very little.&lt;/p&gt;

&lt;p&gt;If I subtract the RSS of an empty WASI module run through the same path, with a 64 MiB maximum linear memory like the benchmark build, only a couple of MiB remain:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/wasm2c2026/memory-extra.png&quot; alt=&quot;What the module itself costs&quot; /&gt;&lt;/p&gt;

&lt;p&gt;At run time, with the WebAssembly code directly compiled to C, then to executable code, there’s no engine process to bring along.&lt;/p&gt;

&lt;h2 id=&quot;no-runtime-is-still-a-good-choice&quot;&gt;No runtime is still a good choice&lt;/h2&gt;

&lt;p&gt;The generated output is portable C.&lt;/p&gt;

&lt;p&gt;So, the deployment target no longer has to be “a platform with a WebAssembly runtime”. It can be any platform with a C compiler and enough libc support for whatever the module imports.&lt;/p&gt;

&lt;p&gt;It also means the compiler is a choice.&lt;/p&gt;

&lt;p&gt;You can compile the generated C with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clang&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gcc&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But you can also take advantage of modern C toolchains such as &lt;a href=&quot;https://fil-c.org/&quot;&gt;Fil-C&lt;/a&gt; to get memory safety on wasm64, or on wasm32 without depending on virtual-memory guard pages.&lt;/p&gt;

&lt;p&gt;Or you can use a formally verified compiler such as &lt;a href=&quot;https://compcert.org/&quot;&gt;CompCert&lt;/a&gt; for high assurance code generation.&lt;/p&gt;

&lt;p&gt;I didn’t benchmark those toolchains. The point is that WebAssembly-to-C gives you that option, while a dedicated runtime doesn’t.&lt;/p&gt;

&lt;p&gt;This is also why I remain impressed by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmer&lt;/code&gt; also implements &lt;a href=&quot;https://wasix.org/&quot;&gt;WASIX&lt;/a&gt;, which fills many of the POSIX-shaped gaps that plain WASI still leaves open. So, if your goal is to run existing applications in WebAssembly with fewer rewrites, this is the way to go, not WebAssembly-to-C or other options.&lt;/p&gt;

&lt;p&gt;And &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmtime&lt;/code&gt; implements the “Component Model”, that improves code separation and gives applications a typed interface boundary for composing pieces of WebAssembly. I have an intuition that the same thing can be achieved using WebAssembly-to-C by compiling components separately, but I didn’t think about it much.&lt;/p&gt;

&lt;p&gt;Regardless WebAssembly-to-C is the wrong answer if you need dynamic loading of untrusted code without precompilation, runtime-level policy, preemption, fuel, component-model machinery, or one engine reused across many tenants.&lt;/p&gt;

&lt;p&gt;It’s also the wrong answer if your deployment model depends on shipping one portable &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.wasm&lt;/code&gt; artifact and letting the destination machine choose how to run it.&lt;/p&gt;

&lt;p&gt;But if you control the WebAssembly module, can precompile it, and don’t need runtime services, WebAssembly-to-C is very close to a no-brainer, especially with the addition of wide arithmetic.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Calling everything AI-generated is lazy</title>
    <link href="https://00f.net/2026/06/25/stop-calling-everything-ai-generated/"/>
   <updated>2026-06-25T00:00:00+02:00</updated>
   <id>https://00f.net/2026/06/25/stop-calling-everything-ai-generated</id>
   <content type="html">&lt;p&gt;The new lazy comment’s “AI-generated”.&lt;/p&gt;

&lt;p&gt;On Hacker News, every thread about a programming language that isn’t Rust eventually gets a Rust comment. It doesn’t matter if the topic’s C, Zig, Go, PHP, Elixir, or a database. Somebody will find a way to turn the discussion into Rust.&lt;/p&gt;

&lt;p&gt;We now have the same reflex for writing.&lt;/p&gt;

&lt;p&gt;Someone publishes a blog post. Maybe something a shower thought. Maybe just a weird thing they spent the weekend trying to understand.&lt;/p&gt;

&lt;p&gt;Instead of pointing to a wrong number, a missing test, a wrong claim, or saying thank you, somebody just writes “sounds AI-generated”.&lt;/p&gt;

&lt;p&gt;That isn’t criticism. It gives the author nothing to fix. No sentence. No claim. No result. Just a little accusation dropped in the middle of the room. Just sending bad vibes for no reason.&lt;/p&gt;

&lt;p&gt;And it lands on real people.&lt;/p&gt;

&lt;p&gt;A lot of personal blogs aren’t content businesses. They’re notes written after work, or on a weekend, or at 4am, while the author’s still excited or annoyed enough to explain what they just found. The research may’ve taken days. The writing may’ve taken hours. The reward’s usually nothing. Maybe one useful comment. Maybe one email years later. Maybe just knowing that the next person won’t lose the same afternoon.&lt;/p&gt;

&lt;p&gt;Calling that “AI-generated” isn’t harmless.&lt;/p&gt;

&lt;p&gt;The garbage sites that actually publish AI slop don’t care. There’s nobody behind them feeling insulted. The pipeline doesn’t read Hacker News. It doesn’t get discouraged. It doesn’t wonder whether publishing was a mistake. It scrapes, generates, publishes, and repeats.&lt;/p&gt;

&lt;p&gt;The person writing a blog does read it.&lt;/p&gt;

&lt;p&gt;I’m French. English isn’t my native language.&lt;/p&gt;

&lt;p&gt;When I write, I don’t first think “how would a native speaker phrase this?” I think about the thing I just did or the idea I want to get out before the excitement’s gone.&lt;/p&gt;

&lt;p&gt;And I use a computer, so yes, I use tools.&lt;/p&gt;

&lt;p&gt;I utterly hate AI-generated content. I don’t want machines to replace personal writing with synthetic filler.&lt;/p&gt;

&lt;p&gt;But AI as an assistant is different.&lt;/p&gt;

&lt;p&gt;Checking. Verifying. Proofreading. Helping me say in English what I already know I want to say. &lt;em&gt;That&lt;/em&gt; is useful.&lt;/p&gt;

&lt;p&gt;I use Apple Intelligence proofreading on my Mac all the time. I ask ChatGPT to fix my English. It fixes grammar. It removes very French sentence structures. It rewrites everything into something that looks a bit too clean. Do you really think I can’t see it too?&lt;/p&gt;

&lt;p&gt;AI-assisted prose has patterns. Any idiot can recognizing its stinky smell.&lt;/p&gt;

&lt;p&gt;But a spell checker doesn’t make a post fake. Grammarly didn’t make old posts fake. A machine helping a non-native speaker write more readable English doesn’t make the work fake.&lt;/p&gt;

&lt;p&gt;The work’s the part before the sentence.&lt;/p&gt;

&lt;p&gt;Did I write from actual experience? Did I put my name on the result and accept being wrong in public? This is what should matter.&lt;/p&gt;

&lt;p&gt;That’s also why the drive-by accusation is so annoying.&lt;/p&gt;

&lt;p&gt;Real AI slop exists. It’s everywhere and it’s awful. But as a filter, “this sounds like AI” is just fkcing annoying.&lt;/p&gt;

&lt;p&gt;Mostly, it detects fluency. That’s exactly what non-native writers use tools for. It punishes people for making their English easier to read. It pushes them toward a stupid choice: write worse and sound human, or write better and become suspicious.&lt;/p&gt;

&lt;p&gt;That sucks.&lt;/p&gt;

&lt;p&gt;If a post’s wrong, say what’s wrong. If a chart’s bogus, show the bug. If a claim’s too strong, argue with the claim. If a paragraph’s clearly hallucinated, quote it.&lt;/p&gt;

&lt;p&gt;But a drive-by “AI-generated” comment’s just a status signal. It says “I noticed the current thing.” It doesn’t improve the discussion. It doesn’t protect anyone. It doesn’t help the author. It only makes publishing feel worse.&lt;/p&gt;

&lt;p&gt;And publishing personal writing already has a cost.&lt;/p&gt;

&lt;p&gt;You expose how you think. You expose what you don’t know. If you write in a second language, you also expose every awkward sentence. There’s always the small fear that the post’s too rough, too direct, too French, too wrong.&lt;/p&gt;

&lt;p&gt;Proofreading tools reduce that friction. They let me share the technical part instead of spending all my energy fighting articles and prepositions.&lt;/p&gt;

&lt;p&gt;If that makes some sentences look assisted, so be it.&lt;/p&gt;

&lt;p&gt;I care more about whether the content’s useful than whether every comma proves I typed it manually.&lt;/p&gt;

&lt;p&gt;The annoying part’s that people keep saying they want the human web back. They want personal blogs, independent notes, small discoveries, weird experiments, people writing outside platforms.&lt;/p&gt;

&lt;p&gt;Then the same people make it unpleasant to publish any of that.&lt;/p&gt;

&lt;p&gt;Haters are gonna hate. Fine.&lt;/p&gt;

&lt;p&gt;But this particular kind of hate costs something. The AI farms survive the comments. Humans may not.&lt;/p&gt;

&lt;p&gt;So no, I won’t apologize for using proofreading tools.&lt;/p&gt;

&lt;p&gt;If you want to fight AI slop, fight empty work. Fight fake expertise. Fight articles with no author, no experience, no accountability, and no point.&lt;/p&gt;

&lt;p&gt;Don’t make humans prove they’re human every time they write a decent sentence.&lt;/p&gt;

&lt;p&gt;That’s how you get fewer humans writing.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Performance of WebAssembly runtimes in 2026</title>
    <link href="https://00f.net/2026/06/23/webassembly-runtimes-2026/"/>
   <updated>2026-06-23T00:00:00+02:00</updated>
   <id>https://00f.net/2026/06/23/webassembly-runtimes-2026</id>
   <content type="html">&lt;p&gt;I wanted to know if WebAssembly runtimes are getting faster.&lt;/p&gt;

&lt;p&gt;This is a follow-up to the earlier libsodium WebAssembly benchmarks from &lt;a href=&quot;/2019/04/09/benchmarking-webassembly-using-libsodium/&quot;&gt;2019&lt;/a&gt;, &lt;a href=&quot;/2021/02/22/webassembly-runtimes-benchmarks/&quot;&gt;2021&lt;/a&gt; and &lt;a href=&quot;/2023/01/04/webassembly-benchmark-2023/&quot;&gt;2023&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Not “does the newest version beat native code in one microbenchmark?”, and not “which runtime has the prettiest benchmark chart?”, but something more boring and more useful:&lt;/p&gt;

&lt;p&gt;If I take the same C crypto code, compile it to WebAssembly, and run it on the latest runtime, a runtime from one year ago, and a runtime from two years ago, are things actually improving?&lt;/p&gt;

&lt;p&gt;So I benchmarked libsodium on WebAssembly runtimes released around June 2024, June 2025, and June 2026.&lt;/p&gt;

&lt;p&gt;The short version:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wasmer&lt;/code&gt; is the best performer, but &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAVM&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAMR&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmtime&lt;/code&gt; are close.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAVM&lt;/code&gt; has the best optimizer, and is able to generate very fast code out from baseline, portable WebAssembly&lt;/li&gt;
  &lt;li&gt;The new WebAssembly &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wide_arithmetic&lt;/code&gt; instructions are a big deal for crypto code when runtimes support them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;what-i-measured&quot;&gt;What I measured&lt;/h2&gt;

&lt;p&gt;The test program is libsodium’s benchmark suite, built from libsodium commit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8e3be8615ba6adcd7babaecf5e76f516890ba5fb&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I built one native baseline and several WebAssembly variants:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;native x86-64, compiled with Zig using the local CPU target&lt;/li&gt;
  &lt;li&gt;plain WebAssembly&lt;/li&gt;
  &lt;li&gt;WebAssembly with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lime1&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;WebAssembly with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lime1&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;simd128&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;WebAssembly with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lime1&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;simd128&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wide_arithmetic&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the native reference, libsodium was built with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-Dcpu=native&lt;/code&gt;. For &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wasm2c&lt;/code&gt;, the generated C was compiled with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;zig cc -O3 -march=native&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAMR&lt;/code&gt;, I used AOT mode: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wamrc&lt;/code&gt; compiled each &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.wasm&lt;/code&gt; file to an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.aot&lt;/code&gt; file, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iwasm&lt;/code&gt; ran the resulting AOT file. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wamrc&lt;/code&gt; doesn’t accept &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--cpu=native&lt;/code&gt;, so I used &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--target=x86_64 --cpu=x86-64-v4 --opt-level=3&lt;/code&gt;, which matches the host’s available x86-64 feature level and works across the WAMR versions that could compile these modules.&lt;/p&gt;

&lt;p&gt;The native command was:&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;zig build &lt;span class=&quot;nt&quot;&gt;-Denable_benchmarks&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-Doptimize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;ReleaseFast &lt;span class=&quot;nt&quot;&gt;-Dcpu&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;native &lt;span class=&quot;nt&quot;&gt;-Diterations&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;3
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The WebAssembly commands were the same shape, with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wasm32-wasi&lt;/code&gt; target and the feature-specific CPU strings:&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;zig build &lt;span class=&quot;nt&quot;&gt;-Denable_benchmarks&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-Dtarget&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;wasm32-wasi &lt;span class=&quot;nt&quot;&gt;-Doptimize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;ReleaseFast &lt;span class=&quot;nt&quot;&gt;-Diterations&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;3
zig build &lt;span class=&quot;nt&quot;&gt;-Denable_benchmarks&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-Dtarget&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;wasm32-wasi &lt;span class=&quot;nt&quot;&gt;-Doptimize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;ReleaseFast &lt;span class=&quot;nt&quot;&gt;-Dcpu&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;lime1 &lt;span class=&quot;nt&quot;&gt;-Diterations&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;3
zig build &lt;span class=&quot;nt&quot;&gt;-Denable_benchmarks&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-Dtarget&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;wasm32-wasi &lt;span class=&quot;nt&quot;&gt;-Doptimize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;ReleaseFast &lt;span class=&quot;nt&quot;&gt;-Dcpu&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;lime1+simd128 &lt;span class=&quot;nt&quot;&gt;-Diterations&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;3
zig build &lt;span class=&quot;nt&quot;&gt;-Denable_benchmarks&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-Dtarget&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;wasm32-wasi &lt;span class=&quot;nt&quot;&gt;-Doptimize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;ReleaseFast &lt;span class=&quot;nt&quot;&gt;-Dcpu&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;lime1+simd128+wide_arithmetic &lt;span class=&quot;nt&quot;&gt;-Diterations&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;3
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The host was an AMD Ryzen AI 9 HX 470 with 12 cores and 24 threads. CPU boost was disabled and the maximum CPU frequency was 2 GHz. The OS was Linux 7.1.0-rc7, and Zig was &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.17.0-dev.948+e949341b7&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The numbers below are the geometric mean of per-benchmark slowdowns relative to the native build. Lower is better. A value of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2.0&lt;/code&gt; means “twice as slow as native” on this machine.&lt;/p&gt;

&lt;p&gt;I used &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ITERATIONS=3&lt;/code&gt;, so the very small libsodium tests are noisy and quantized. Rows reporting zero time were excluded from the aggregate.&lt;/p&gt;

&lt;h2 id=&quot;versions&quot;&gt;Versions&lt;/h2&gt;

&lt;p&gt;For every runtime except &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAVM&lt;/code&gt;, I used the latest stable release available on June 23, 2026, plus a stable release from roughly one year earlier and one from roughly two years earlier.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Runtime&lt;/th&gt;
      &lt;th&gt;2024&lt;/th&gt;
      &lt;th&gt;2025&lt;/th&gt;
      &lt;th&gt;2026&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Bun&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/oven-sh/bun/releases/tag/bun-v1.1.16&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.1.16&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/oven-sh/bun/releases/tag/bun-v1.2.17&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.2.17&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/oven-sh/bun/releases/tag/bun-v1.3.14&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.3.14&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Node&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://nodejs.org/dist/v22.3.0/&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;22.3.0&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://nodejs.org/dist/v24.2.0/&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;24.2.0&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://nodejs.org/dist/v26.3.1/&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;26.3.1&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAMR&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/bytecodealliance/wasm-micro-runtime/releases/tag/WAMR-2.1.0&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2.1.0&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/bytecodealliance/wasm-micro-runtime/releases/tag/WAMR-2.3.1&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2.3.1&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/bytecodealliance/wasm-micro-runtime/releases/tag/WAMR-2.4.4&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2.4.4&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WABT wasm2c&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/WebAssembly/wabt/releases/tag/1.0.35&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.0.35&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/WebAssembly/wabt/releases/tag/1.0.37&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.0.37&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/WebAssembly/wabt/releases/tag/1.0.41&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.0.41&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WasmEdge&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/WasmEdge/WasmEdge/releases/tag/0.14.0&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.14.0&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/WasmEdge/WasmEdge/releases/tag/0.14.1&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.14.1&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/WasmEdge/WasmEdge/releases/tag/0.17.0&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.17.0&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmer&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/wasmerio/wasmer/releases/tag/v4.3.2&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4.3.2&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/wasmerio/wasmer/releases/tag/v6.0.1&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;6.0.1&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/wasmerio/wasmer/releases/tag/v7.1.0&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;7.1.0&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmtime&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/bytecodealliance/wasmtime/releases/tag/v22.0.0&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;22.0.0&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/bytecodealliance/wasmtime/releases/tag/v34.0.0&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;34.0.0&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/bytecodealliance/wasmtime/releases/tag/v46.0.0&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;46.0.0&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAVM&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;n/a&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;n/a&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/WAVM/WAVM/releases/tag/nightly/2026-04-05&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nightly/2026-04-05&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wazero&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/wazero/wazero/releases/tag/v1.7.3&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.7.3&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/wazero/wazero/releases/tag/v1.9.0&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.9.0&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/wazero/wazero/releases/tag/v1.12.0&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.12.0&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAVM&lt;/code&gt; is awkward to compare historically. The old available nightly collapsed to a 2022 binary for both the 2024 and 2025 slots, and that binary refused to run on this machine. I only kept the 2026 nightly.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAMR 2.1.0&lt;/code&gt;, the selected 2024 release, installed fine but its AOT compiler failed on these Zig-generated modules with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;invalid WASM stack data type&lt;/code&gt;. I kept the version in the matrix, but didn’t include an aggregate for it.&lt;/p&gt;

&lt;h2 id=&quot;baseline-webassembly&quot;&gt;Baseline WebAssembly&lt;/h2&gt;

&lt;p&gt;This is the plain WebAssembly build, without &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lime1&lt;/code&gt;, SIMD, or wide arithmetic.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/wasm2026/baseline.png&quot; alt=&quot;Baseline WebAssembly slowdown by release year&quot; /&gt;&lt;/p&gt;

&lt;p&gt;There isn’t one universal trend.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmtime&lt;/code&gt; steadily improved: 2.67x native in 2024, 2.54x in 2025, 2.41x in 2026. It got faster every year, and the gains land in the tenths place, above the noise.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Node&lt;/code&gt; also improved slowly, from 8.60x native to 7.95x native.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wazero&lt;/code&gt; was basically flat: 4.84x, 4.70x, 4.72x native. No real movement over two years.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAMR&lt;/code&gt; in AOT mode was already fast in 2025 and stayed there in 2026: 1.59x native, then 1.57x native, the same within this benchmark’s noise. I don’t have a complete 2024 WAMR number because &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAMR 2.1.0&lt;/code&gt; couldn’t compile these modules.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmer&lt;/code&gt; regressed in the 2025 release I tested, then recovered in 2026. The 2026 baseline barely beats 2024.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wasm2c&lt;/code&gt; improved modestly in 2026. It remains one of the best options if ahead-of-time translation to native C is acceptable for your deployment model.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Bun&lt;/code&gt; is the outlier. Its 2024 and 2025 results were far behind, but the 2026 result is about three times faster than the 2025 result. It’s still slower than &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Node&lt;/code&gt; on this benchmark, but the direction is excellent.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WasmEdge&lt;/code&gt; is fast too, but its command-line behavior changed enough to matter. My first &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.17.0&lt;/code&gt; run accidentally used interpreter mode for compiled modules and looked catastrophically slow. Running the compiled modules with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--run-mode=aot&lt;/code&gt; fixed it: the 2026 baseline was 1.74x native, between the 2024 and 2025 baseline results.&lt;/p&gt;

&lt;h2 id=&quot;best-supported-build-by-year&quot;&gt;Best supported build by year&lt;/h2&gt;

&lt;p&gt;The baseline table is useful because it compares the same WebAssembly target everywhere.&lt;/p&gt;

&lt;p&gt;But if you’re choosing a runtime for your own deployment, you probably care about the fastest build that runtime can actually run.&lt;/p&gt;

&lt;p&gt;So for each runtime and year, I also selected the best complete result among the supported builds: baseline, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lime1&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lime1+simd128&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lime1+simd128+wide_arithmetic&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/wasm2026/best-by-year.png&quot; alt=&quot;Best supported build by release year&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Looks similar to the previous graph, except for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmtime&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmer&lt;/code&gt; that really benefit from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wide_arithmetic&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Ranked by the best supported build, the complete current-year results are:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/wasm2026/rank-2026.png&quot; alt=&quot;2026 releases, best supported build, ranked&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;cpu-feature-variants&quot;&gt;CPU feature variants&lt;/h2&gt;

&lt;p&gt;The WebAssembly feature story is more interesting than the year-to-year runtime story.&lt;/p&gt;

&lt;p&gt;For the 2026 releases, these were the aggregate slowdowns:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/wasm2026/features-2026.png&quot; alt=&quot;CPU feature variants across 2026 releases&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lime1&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;simd128&lt;/code&gt; alone aren’t magic here. Sometimes they help, sometimes they hurt, and sometimes the difference is lost in benchmark noise.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wide_arithmetic&lt;/code&gt; is different.&lt;/p&gt;

&lt;p&gt;Only &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmtime&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmer&lt;/code&gt; could run the full &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wide_arithmetic&lt;/code&gt; build among the complete stable rows I tested. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAMR&lt;/code&gt; rejected it with unsupported opcode &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0xfc13&lt;/code&gt;. But when &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wide_arithmetic&lt;/code&gt; worked, it was the biggest speedup in the whole experiment:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmtime 46.0.0&lt;/code&gt;: 2.41x native without it, 1.46x native with it.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmer 7.1.0&lt;/code&gt;: 2.08x native without it, 1.33x native with it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s the kind of change I like. A lot of libsodium’s expensive operations are arithmetic-heavy. If the WebAssembly ISA can express that arithmetic directly, the runtime has much less work to rediscover what the C compiler already knew.&lt;/p&gt;

&lt;h2 id=&quot;failures&quot;&gt;Failures&lt;/h2&gt;

&lt;p&gt;Most runs completed cleanly, but not all of them.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Bun 1.2.17&lt;/code&gt; failed &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;box_easy&lt;/code&gt; in the baseline build. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Bun 1.1.16&lt;/code&gt; failed &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pwhash_argon2i&lt;/code&gt; in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lime1&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lime1+simd128&lt;/code&gt; builds.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Node 22.3.0&lt;/code&gt; originally failed &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pwhash_argon2i&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pwhash_argon2id&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pwhash_scrypt&lt;/code&gt; in the baseline, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lime1&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lime1+simd128&lt;/code&gt; builds. The failures weren’t fixed by increasing Node’s JavaScript heap or stack settings. They were fixed by giving the Wasm modules an explicit maximum linear memory. With the baseline build, a 1024-page maximum, or 64 MiB, made &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pwhash_argon2i&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pwhash_argon2id&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pwhash_scrypt&lt;/code&gt; complete. But &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pwhash_scrypt&lt;/code&gt; failed with 512 pages and segfaulted again at 1536 pages and above, so this appears to be a V8 memory-mode threshold rather than a simple “more memory is better” setting.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAMR 2.1.0&lt;/code&gt;, the 2024 slot, couldn’t compile even the baseline modules in AOT mode. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAMR 2.3.1&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2.4.4&lt;/code&gt; compiled and ran the baseline, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lime1&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lime1+simd128&lt;/code&gt; builds, but not &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wide_arithmetic&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Those failures were excluded from the aggregate. So were benchmark rows with a zero reported median.&lt;/p&gt;

&lt;h2 id=&quot;so-are-runtimes-getting-faster&quot;&gt;So, are runtimes getting faster?&lt;/h2&gt;

&lt;p&gt;Some of them are.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmtime&lt;/code&gt; is the cleanest yes: it got faster every year in this benchmark, by a little each time.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Node&lt;/code&gt; is also a yes, but the slope is gentle.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Bun&lt;/code&gt; is a loud yes between 2025 and 2026. It still has a lot of ground to cover for this workload, but the improvement is too large to ignore.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wazero&lt;/code&gt; is mostly flat.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAMR&lt;/code&gt; is also mostly flat between the versions that worked here, but “flat” at about 1.4x to 1.6x native is a very good place to be.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmer&lt;/code&gt; is mixed if you only look at the baseline, but the 2026 release supporting &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wide_arithmetic&lt;/code&gt; changes the practical answer for crypto code. With that feature enabled, it was the fastest complete 2026 result I could compare across a normal current release.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wasm2c&lt;/code&gt; remains good. If you can translate WebAssembly to C ahead of time and compile it for the host, it’s hard to beat.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAVM&lt;/code&gt; produced the fastest 2026 baseline number, but I don’t have a fair 2024 or 2025 comparison.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WasmEdge&lt;/code&gt; remains excellent once it’s forced into AOT mode. The accidental interpreter-mode run was a good reminder that command-line defaults are part of the benchmark, too.&lt;/p&gt;

&lt;h2 id=&quot;takeaways&quot;&gt;Takeaways&lt;/h2&gt;

&lt;p&gt;If you run CPU-heavy cryptography in WebAssembly, runtime choice still matters a lot.&lt;/p&gt;

&lt;p&gt;The spread between the fastest complete current result and the slowest current result is large: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmer&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wide_arithmetic&lt;/code&gt; was 1.33x native, while current &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Bun&lt;/code&gt; baseline was 8.77x native.&lt;/p&gt;

&lt;p&gt;Feature support matters too. The same runtime can move from “pretty good” to “surprisingly close to native” when the WebAssembly module can use better arithmetic instructions.&lt;/p&gt;

&lt;p&gt;The comforting part is that the mainstream runtimes aren’t standing still. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmtime&lt;/code&gt; improved steadily. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Bun&lt;/code&gt; made a huge jump. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmer&lt;/code&gt; gained a feature that matters for real crypto workloads. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WasmEdge&lt;/code&gt; remained fast once the AOT run mode was explicit.&lt;/p&gt;

&lt;p&gt;The less comforting part is that WebAssembly performance still isn’t one thing. It depends on the runtime, the release, the enabled WebAssembly features, whether the code goes through WASI from JavaScript, and whether ahead-of-time native compilation is allowed.&lt;/p&gt;

&lt;p&gt;So benchmark your actual workload.&lt;/p&gt;

&lt;p&gt;But if your workload looks like libsodium, the answer in 2026 is: WebAssembly can be close to native, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wide_arithmetic&lt;/code&gt; is worth caring about, and yes, some runtimes really are getting faster.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Faster signatures for WebAssembly</title>
    <link href="https://00f.net/2026/06/17/ed25519-wasm-signatures/"/>
   <updated>2026-06-17T00:00:00+02:00</updated>
   <id>https://00f.net/2026/06/17/ed25519-wasm-signatures</id>
   <content type="html">&lt;p&gt;I just released &lt;a href=&quot;https://github.com/jedisct1/rust-ed25519-wasm&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ed25519-wasm&lt;/code&gt;&lt;/a&gt;, a small Rust crate for Ed25519 signatures in WebAssembly. The static library can also be directly linked in applications written in other languages.&lt;/p&gt;

&lt;p&gt;It’s backed by &lt;a href=&quot;https://lib25519.cr.yp.to/&quot;&gt;lib25519&lt;/a&gt;, compiled to WebAssembly using Zig, linked to Rust bindings, and exposes a simple API similar to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ed25519-compact&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-rust highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;ed25519_wasm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;KeyPair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Seed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key_pair&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;KeyPair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;from_seed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nn&quot;&gt;Seed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;generate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;message&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;b&quot;hello&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;signature&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key_pair&lt;/span&gt;&lt;span class=&quot;py&quot;&gt;.sk&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.sign&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;key_pair&lt;/span&gt;&lt;span class=&quot;py&quot;&gt;.pk&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.verify&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;signature&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.unwrap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;the-benchmark&quot;&gt;The benchmark&lt;/h2&gt;

&lt;p&gt;The benchmark in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ed25519-wasm&lt;/code&gt; README was run with Wasmtime 45.0.2, using 128-byte messages.&lt;/p&gt;

&lt;p&gt;The table reports the median time per operation and the relative speed compared to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ed25519-wasm&lt;/code&gt;:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Algorithm / implementation&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Sign 128B&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Verify 128B&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Sign vs ed25519-wasm&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Verify vs ed25519-wasm&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Ed25519 / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ed25519-wasm&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;15.1 us&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;49.1 us&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1.0x&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1.0x&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Ed25519 / libsodium wasm&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;21.4 us&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;53.1 us&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1.4x slower&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1.1x slower&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;ECDSA-P256/SHA-256 / RustCrypto &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p256&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;245 us&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;277 us&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;16.2x slower&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;5.6x slower&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;ML-DSA-44 / Zig std wasm&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;135.5 us&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;23.3 us&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;8.9x slower&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;2.1x faster&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;ML-DSA-44 / RustCrypto &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ml-dsa&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;706 us&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;91.7 us&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;46.6x slower&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1.9x slower&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;RSA-2048 / RustCrypto &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rsa&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;3.21 ms&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;171 us&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;212x slower&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;3.5x slower&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;RSA-3072 / RustCrypto &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rsa&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;10.2 ms&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;394 us&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;671x slower&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;8.0x slower&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ed25519&lt;/code&gt; beats everything hands down.&lt;/p&gt;

&lt;p&gt;If you already use libsodium, including from Rust through something like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libsodium-rs&lt;/code&gt;, that’s a perfectly reasonable option.&lt;/p&gt;

&lt;p&gt;But &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ed25519-wasm&lt;/code&gt; is slightly faster: 15.1 microseconds to sign and 49.1 microseconds to verify a 128-byte message on Apple M4.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;P-256&lt;/code&gt; isn’t close.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ECDSA-P256/SHA-256&lt;/code&gt; with RustCrypto’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p256&lt;/code&gt; takes 245 microseconds to sign and 277 microseconds to verify. That’s 16.2 times slower for signing and 5.6 times slower for verification.&lt;/p&gt;

&lt;p&gt;RSA is much worse.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RSA-2048&lt;/code&gt; signing takes 3.21 milliseconds. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RSA-3072&lt;/code&gt; signing takes 10.2 milliseconds.&lt;/p&gt;

&lt;p&gt;Compared to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ed25519-wasm&lt;/code&gt;, that’s &lt;strong&gt;212 times slower&lt;/strong&gt; and &lt;strong&gt;671 times slower&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Verification is less catastrophic, but still bad: 171 microseconds for RSA-2048 and 394 microseconds for RSA-3072.&lt;/p&gt;

&lt;h2 id=&quot;rs256-is-a-huge-waste-of-cpu-cycles-and-money&quot;&gt;RS256 is a huge waste of CPU cycles and money&lt;/h2&gt;

&lt;p&gt;Even when they are not forced to, people use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JWT&lt;/code&gt;. Especially &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RS256&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JWT&lt;/code&gt;’s jargon, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RS256&lt;/code&gt; means RSA with SHA-256. It’s common because old systems support it, cloud identity products support it, tutorials use it, and people copy what already works.&lt;/p&gt;

&lt;p&gt;But if you’re verifying or signing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JWT&lt;/code&gt; tokens in WebAssembly, avoid &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RS256&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For signing, it isn’t even close. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RSA-2048&lt;/code&gt; is already &lt;strong&gt;212 times slower&lt;/strong&gt; than &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ed25519-wasm&lt;/code&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RSA-3072&lt;/code&gt; is &lt;strong&gt;671 times slower&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Even verification, the operation &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JWT&lt;/code&gt; consumers usually care about, is &lt;strong&gt;3.5 times slower&lt;/strong&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RSA-2048&lt;/code&gt; and &lt;strong&gt;8 times slower&lt;/strong&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RSA-3072&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If this runs in a browser once, maybe you don’t care.&lt;/p&gt;

&lt;p&gt;If this runs at the edge for every request, or in a WebAssembly function where CPU time is literally what you pay for, you should care a lot. It’s a terrible choice.&lt;/p&gt;

&lt;h2 id=&quot;what-about-post-quantum-signatures&quot;&gt;What about post-quantum signatures?&lt;/h2&gt;

&lt;p&gt;ML-DSA is interesting.&lt;/p&gt;

&lt;p&gt;It’s post-quantum resistant, and in WebAssembly, its performance is close to native since it’s mostly based on scalar arithmetic.&lt;/p&gt;

&lt;p&gt;The Zig standard library implementation of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ML-DSA-44&lt;/code&gt; compiled to WebAssembly signs in 135.5 microseconds and verifies in 23.3 microseconds in this benchmark.&lt;/p&gt;

&lt;p&gt;This is actually faster than Ed25519!&lt;/p&gt;

&lt;p&gt;But don’t generalize that to every ML-DSA implementation.&lt;/p&gt;

&lt;p&gt;RustCrypto’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ml-dsa&lt;/code&gt; implementation is much slower, and signs in 706 microseconds and verifies in 91.7 microseconds. That makes signing 46.6 times slower than &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ed25519-wasm&lt;/code&gt;, and even verification is 1.9 times slower.&lt;/p&gt;

&lt;p&gt;But with a fast implementation, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ML-DSA&lt;/code&gt; is a wise choice for WebAssembly if you can afford the large keys and signatures.&lt;/p&gt;

&lt;h2 id=&quot;use-ed25519-or-ml-dsa-forget-rsa&quot;&gt;Use Ed25519 or ML-DSA, forget RSA&lt;/h2&gt;

&lt;p&gt;For most WebAssembly applications that need signatures, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ed25519&lt;/code&gt; should be your go-to algorithm.&lt;/p&gt;

&lt;p&gt;It’s got small keys, small signatures, simple APIs, fast signing, fast verification, and mature implementations.&lt;/p&gt;

&lt;p&gt;If you already ship libsodium to WebAssembly, keep using it. The WebAssembly build performs well, and libsodium remains a good default crypto toolbox.&lt;/p&gt;

&lt;p&gt;But if the only thing you need is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ed25519&lt;/code&gt; signatures from Rust targeting WebAssembly, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ed25519-wasm&lt;/code&gt; is now the best option I know of.&lt;/p&gt;

&lt;p&gt;It’s small. It’s direct. It’s backed by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lib25519&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you pay to run WebAssembly code, use this.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Feeding a PRF its own tail</title>
    <link href="https://00f.net/2026/06/16/feeding-a-prf-its-own-tail/"/>
   <updated>2026-06-16T00:00:00+02:00</updated>
   <id>https://00f.net/2026/06/16/feeding-a-prf-its-own-tail</id>
   <content type="html">&lt;p&gt;A while back, while reviewing code for a customer, I stumbled on a custom RNG. It was used for pretty much everything in the application, including generating supposedly unique identifiers.&lt;/p&gt;

&lt;p&gt;A custom RNG is usually a red flag, so I took a closer look.&lt;/p&gt;

&lt;p&gt;But before getting into how it works, we need to talk about pseudorandom functions.&lt;/p&gt;

&lt;h2 id=&quot;on-prfs&quot;&gt;On PRFs&lt;/h2&gt;

&lt;p&gt;A pseudorandom function (PRF) takes a key and some input data, and produces a fixed-size output:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;PRF(key, data) -&amp;gt; output
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Informally, in a secure PRF, given any number of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(data, output)&lt;/code&gt; pairs, recovering the key is as hard as a brute-force search.&lt;/p&gt;

&lt;p&gt;More importantly, no matter how many distinct &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(data, output)&lt;/code&gt; pairs you have already seen, for a new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data&lt;/code&gt; input any possible output is equally likely. Seeing an output before doesn’t mean a different input can’t produce it again. Unlike a permutation, the security of the function doesn’t degrade as the number of inputs grows.&lt;/p&gt;

&lt;p&gt;A common example is SipHash: it takes a 128-bit key and a variable-length input, and produces a 64- or 128-bit output.&lt;/p&gt;

&lt;h2 id=&quot;the-custom-random-number-generator&quot;&gt;The custom random number generator&lt;/h2&gt;

&lt;p&gt;The custom RNG worked like this:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;SipHash(key, state) -&amp;gt; new_state, output
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The key is generated from the kernel RNG when the application starts, and never changes afterwards.&lt;/p&gt;

&lt;p&gt;There’s also a special case with a static key, where callers of the public API can set the initial state themselves.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;state&lt;/code&gt; is initialized to a different value (the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;seed&lt;/code&gt;) for each thread.&lt;/p&gt;

&lt;p&gt;Each call splits the function’s 128-bit output into two 64-bit words: the first half overwrites the state, the second half is handed back to the caller as random bytes.&lt;/p&gt;

&lt;p&gt;At first sight, the generator may feel reasonable. A 2&lt;sup&gt;64&lt;/sup&gt; domain is borderline, but for most protocols it might be acceptable &lt;em&gt;in practice&lt;/em&gt;. The PRF is indistinguishable from random under its key, the state never sits still, and every output depends on the full secret. What could go wrong?&lt;/p&gt;

&lt;p&gt;Quite a lot, as it turns out.&lt;/p&gt;

&lt;h2 id=&quot;the-rho-shaped-graph&quot;&gt;The rho-shaped graph&lt;/h2&gt;

&lt;p&gt;A pseudorandom function is not a permutation. Two different inputs are perfectly free to map to the same output.&lt;/p&gt;

&lt;p&gt;And on average, they do: about 37% of the codomain is never hit at all.&lt;/p&gt;

&lt;p&gt;What does that mean for our random number generator?&lt;/p&gt;

&lt;p&gt;It means the sequence of states is a walk on a graph where every node has exactly one outgoing edge and an unpredictable number of incoming edges. That walk always has the same shape: a tail of states that are never revisited, leading into a cycle that loops forever. Like the Greek letter &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ρ&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With a 2&lt;sup&gt;64&lt;/sup&gt; state, on average we fall into a loop after about 2&lt;sup&gt;32&lt;/sup&gt; steps. That’s very small. It’s entirely practical to generate, store, and index the whole cycle on a laptop, and then, given any output, predict the ones that follow.&lt;/p&gt;

&lt;p&gt;Feeding a random function its own output gives you about &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;√N&lt;/code&gt; outputs, not &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;N&lt;/code&gt;. Every doubling of state size buys you one extra bit of effective period, not two.&lt;/p&gt;

&lt;h2 id=&quot;threads-end-up-agreeing-with-each-other&quot;&gt;Threads end up agreeing with each other&lt;/h2&gt;

&lt;p&gt;The RNG has another failure mode. All threads share a common key, but each starts from a different seed.&lt;/p&gt;

&lt;p&gt;So they start at different nodes of the functional graph. But the graph is fixed, and every node eventually leads into one of a small number of cycles. How small?&lt;/p&gt;

&lt;p&gt;A random function on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;N&lt;/code&gt; elements has, on average, about &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;½ ln N&lt;/code&gt; distinct cycles. For N = 2&lt;sup&gt;64&lt;/sup&gt;, that’s roughly twenty-two. Just twenty-two attractors, into which the entire 2&lt;sup&gt;64&lt;/sup&gt;-element state space drains.&lt;/p&gt;

&lt;p&gt;So no matter how carefully you seeded your threads with independent entropy, after each one walks its tail of a few billion steps, it ends up trapped on one of those twenty-two cycles.&lt;/p&gt;

&lt;p&gt;From that point on, the threads are no longer producing independent streams. They are producing rotations of one of a handful of fixed sequences.&lt;/p&gt;

&lt;p&gt;The long-run behavior of the system collapses onto a tiny shared set of orbits.&lt;/p&gt;

&lt;p&gt;For applications where the threads’ streams are supposed to be independent, this is bad. The threads end up generating the same sequences.&lt;/p&gt;

&lt;h2 id=&quot;short-cycles-in-prfs&quot;&gt;Short cycles in PRFs&lt;/h2&gt;

&lt;p&gt;With a PRF and a 64-bit input, about 2&lt;sup&gt;32&lt;/sup&gt; is the longest cycle you can hope for, and the variance around it is large. Depending on the seed and the exact function, the period can be far shorter.&lt;/p&gt;

&lt;p&gt;For example, directly feeding SipHash with a 64-bit output back as the next state, and with key &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(0, 1)&lt;/code&gt; and seed &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0x0000004de9886527&lt;/code&gt; the loop closes after only 16,933,653 steps, more than two hundred times shorter than the 2&lt;sup&gt;32&lt;/sup&gt; you’d expect on average.&lt;/p&gt;

&lt;p&gt;Nothing stops a pseudorandom function from cycling faster still, all the way down to a fixed point: a state that maps to itself, so the generator emits the same value forever. Fortunately, SipHash has only one fixed point: the all-zero state, unreachable for any key. Hard to hit by accident.&lt;/p&gt;

&lt;h2 id=&quot;backtracking-resistance&quot;&gt;Backtracking resistance&lt;/h2&gt;

&lt;p&gt;If the key and some outputs leak, it’s not only possible to predict every future output. It’s also possible to recompute the previous ones.&lt;/p&gt;

&lt;p&gt;No preimage search needed: the cycles are short enough that it’s more efficient to just compute them.&lt;/p&gt;

&lt;h2 id=&quot;a-minimal-fix&quot;&gt;A minimal fix&lt;/h2&gt;

&lt;p&gt;This random number generator is obviously flawed and must not be used in any security context.&lt;/p&gt;

&lt;p&gt;It’s also very inefficient, producing only 64 bits per call.&lt;/p&gt;

&lt;p&gt;A minimal fix would be to use per-thread keys and a counter:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;SipHash(thread_key, counter) -&amp;gt; output
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Incrementing the counter after every produced output builds a sequence that won’t repeat until the counter wraps, at which point you should rekey, and you’ll be safe for about 2&lt;sup&gt;96&lt;/sup&gt; outputs. As a bonus, it’s twice as fast as the original generator: 128 bits per call instead of 64.&lt;/p&gt;

&lt;p&gt;That doesn’t buy any backtracking resistance, though. The keys still need frequent updates.&lt;/p&gt;

&lt;p&gt;I’m also not convinced SipHash is a good fit here, given the input size. AES would be faster, and fine as long as it’s rekeyed before 2&lt;sup&gt;32&lt;/sup&gt; outputs. Or, if you can use it, AES-PRF (&lt;a href=&quot;https://eprint.iacr.org/2017/812&quot;&gt;1&lt;/a&gt;, &lt;a href=&quot;https://tosc.iacr.org/index.php/ToSC/article/view/892/843&quot;&gt;2&lt;/a&gt;) lets you go up to 2&lt;sup&gt;64&lt;/sup&gt; output blocks before rekeying, keeping the generic distinguishing advantage around 2&lt;sup&gt;-128&lt;/sup&gt;, at no performance cost.&lt;/p&gt;

&lt;h2 id=&quot;a-better-fix&quot;&gt;A better fix&lt;/h2&gt;

&lt;p&gt;It’s generally hard to justify building a custom random number generator. These days, the standard library of your favorite language probably ships efficient functions to safely generate random numbers. Use them.&lt;/p&gt;

&lt;p&gt;If you really need to build an RNG, be very careful. Use at least a 128-bit or 256-bit state.&lt;/p&gt;

&lt;p&gt;Daniel Bernstein’s post on &lt;a href=&quot;https://blog.cr.yp.to/20170723-random.html&quot;&gt;fast-key-erasure random-number generators&lt;/a&gt; remains an excellent reference.&lt;/p&gt;

&lt;p&gt;Want something fancier and faster than AES-CTR? Use &lt;a href=&quot;https://eprint.iacr.org/2022/1534&quot;&gt;ButterKnife&lt;/a&gt; in counter mode (similar to &lt;a href=&quot;https://eprint.iacr.org/2023/781&quot;&gt;Skye&lt;/a&gt;). It outputs 1024 bits per call, and the first 256 can be used as the next tweak and key. &lt;a href=&quot;https://github.com/jedisct1/go-aes#butterknife-fast-key-erasure-rng&quot;&gt;PoC&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Want something based on a public permutation? I like &lt;a href=&quot;https://eprint.iacr.org/2016/886.pdf&quot;&gt;Reverie&lt;/a&gt; a lot.&lt;/p&gt;

&lt;p&gt;But realistically, you probably don’t need any of that.&lt;/p&gt;

&lt;h2 id=&quot;what-happened-next&quot;&gt;What happened next&lt;/h2&gt;

&lt;p&gt;I reported all of this. Years later, the generator itself was never touched; the only change was a line added to the documentation noting that it “should not be used for cryptographic purposes”.&lt;/p&gt;

&lt;p&gt;Sad.&lt;/p&gt;
</content>
  </entry>
  
</feed>

