<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Zubair</title>
    <description>The latest articles on DEV Community by Zubair (@2u841r).</description>
    <link>https://dev.to/2u841r</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1269245%2Fc3bc6dda-d540-40d7-b70f-5edd3895a2e4.jpeg</url>
      <title>DEV Community: Zubair</title>
      <link>https://dev.to/2u841r</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/2u841r"/>
    <language>en</language>
    <item>
      <title>axum vs actix-web vs Rocket on 1 CPU and 1 GB RAM</title>
      <dc:creator>Zubair</dc:creator>
      <pubDate>Mon, 02 Nov 2026 03:42:34 +0000</pubDate>
      <link>https://dev.to/2u841r/axum-vs-actix-web-vs-rocket-on-1-cpu-and-1-gb-ram-k16</link>
      <guid>https://dev.to/2u841r/axum-vs-actix-web-vs-rocket-on-1-cpu-and-1-gb-ram-k16</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I wrote the same Todo API in axum, actix-web and Rocket, then load tested each one in Docker with 1 or 2 CPU cores and 1 GB of RAM. On one core, actix served about 67k requests per second, axum 47k and Rocket 34k. Each server idled at about 1 MB of RAM. I then re-ran it on two AWS spot instances in Mumbai, a &lt;code&gt;c7i.xlarge&lt;/code&gt; for the server and a &lt;code&gt;c7i.2xlarge&lt;/code&gt; for the load generator, and got the same ranking over a real network. Before any of that, review found a bug in my axum version that let one bad request take the whole server down, and two settings that would have made the benchmark unfair. Code and full results: &lt;a href="https://github.com/2u841r/actix-vs-axum-vs-rocket" rel="noopener noreferrer"&gt;github.com/2u841r/actix-vs-axum-vs-rocket&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why
&lt;/h2&gt;

&lt;p&gt;Every "Rust web framework benchmark" I find runs on a 64-core machine and reports millions of requests per second. That tells me nothing. The servers I actually pay for have 1 or 2 vCPUs and 1 GB of RAM. I wanted to know how much each framework can handle on &lt;strong&gt;that&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API
&lt;/h2&gt;

&lt;p&gt;Five routes, identical in all three projects:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/health&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;returns &lt;code&gt;OK&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/todos&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;lists all todos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/todos/{id}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;one todo, or 404&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/todos&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;creates a todo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DELETE&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/todos/{id}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;deletes, 204&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;State is a &lt;code&gt;Mutex&amp;lt;HashMap&amp;lt;u64, Todo&amp;gt;&amp;gt;&lt;/code&gt; plus an &lt;code&gt;AtomicU64&lt;/code&gt; for ids. Nothing clever, and the same in all three, so I compare the frameworks and not my data structures.&lt;/p&gt;

&lt;p&gt;The axum version is the shortest of the three:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/health"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(||&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"OK"&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
    &lt;span class="nf"&gt;.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/todos"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;list_todos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;create_todo&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/todos/{id}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_todo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delete_todo&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;.with_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The bug that killed the server
&lt;/h2&gt;

&lt;p&gt;My first axum &lt;code&gt;get_todo&lt;/code&gt; looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="py"&gt;.todos&lt;/span&gt;&lt;span class="nf"&gt;.lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.cloned&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It compiles and passes clippy pedantic, and it works for every id that exists. Ask for one that doesn't and the last &lt;code&gt;unwrap()&lt;/code&gt; panics.&lt;/p&gt;

&lt;p&gt;The panic alone wouldn't be fatal, since axum only drops that connection. The problem is &lt;strong&gt;when&lt;/strong&gt; it panics. The &lt;code&gt;MutexGuard&lt;/code&gt; from &lt;code&gt;lock()&lt;/code&gt; is a temporary, and temporaries live until the end of the statement. So the panic happens while the lock is still held, which &lt;strong&gt;poisons the mutex&lt;/strong&gt;. From then on, every &lt;code&gt;lock().unwrap()&lt;/code&gt; in every handler panics too.&lt;/p&gt;

&lt;p&gt;One &lt;code&gt;GET /todos/999&lt;/code&gt; and the server is effectively dead.&lt;/p&gt;

&lt;p&gt;The fix is to return a 404 instead of panicking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;get_todo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;State&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Arc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Json&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Todo&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="py"&gt;.todos&lt;/span&gt;&lt;span class="nf"&gt;.lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.cloned&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.ok_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;NOT_FOUND&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I first tried returning &lt;code&gt;Option&amp;lt;Json&amp;lt;Todo&amp;gt;&amp;gt;&lt;/code&gt;, which Rocket handles as a 404. axum 0.8 no longer implements &lt;code&gt;IntoResponse&lt;/code&gt; for &lt;code&gt;Option&lt;/code&gt;, so it doesn't compile there. &lt;code&gt;Result&lt;/code&gt; with a status code works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the comparison fair
&lt;/h2&gt;

&lt;p&gt;Two settings would have skewed the results before I ran anything:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rocket logged every request.&lt;/strong&gt; I had built its config from &lt;code&gt;Config::debug_default()&lt;/code&gt;, which logs at the normal level. Printing a line per request at 30k req/s costs a lot, and the other two log nothing. I switched to &lt;code&gt;release_default()&lt;/code&gt; with &lt;code&gt;log_level: LogLevel::Off&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;actix had a hardcoded &lt;code&gt;.workers(8)&lt;/code&gt;.&lt;/strong&gt; Tokio and Rocket size their thread pools from the CPU count. With 1 CPU, actix would have run 8 workers against everyone else's 1. I removed it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All three frameworks call &lt;code&gt;std::thread::available_parallelism()&lt;/code&gt;, which respects both the container's cgroup limit and its CPU pinning. Inside a container pinned to one core, &lt;code&gt;nproc&lt;/code&gt; prints &lt;code&gt;1&lt;/code&gt; and each framework starts one worker. That is what makes the comparison equal.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;One shared multi-stage Dockerfile builds all three:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;rust:1-bookworm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; APP&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /src&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ${APP}/ ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;cargo build &lt;span class="nt"&gt;--release&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cp &lt;/span&gt;target/release/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; /app

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; debian:bookworm-slim&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app /app&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["/app"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each server runs like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--network&lt;/span&gt; host &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cpuset-cpus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="nt"&gt;--memory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1g &lt;span class="nt"&gt;--memory-swap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1g &lt;span class="se"&gt;\&lt;/span&gt;
  axum-api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--cpuset-cpus=0&lt;/code&gt; pins it to one physical core (&lt;code&gt;0,1&lt;/code&gt; for the two-core test).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--memory-swap&lt;/code&gt; equal to &lt;code&gt;--memory&lt;/code&gt; turns swap off, so going over 1 GB gets the server killed instead of slowly swapping.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--network host&lt;/code&gt; skips docker-proxy, which would otherwise add its own overhead to every request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My laptop has a Ryzen 5 8645HS with 6 cores and 12 threads. &lt;code&gt;lscpu -e=CPU,CORE&lt;/code&gt; shows that CPUs 6 and 7 are the hyperthread siblings of 0 and 1. I left them idle and pinned the load generator, &lt;a href="https://github.com/hatoo/oha" rel="noopener noreferrer"&gt;oha&lt;/a&gt;, to the remaining cores:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;taskset &lt;span class="nt"&gt;-c&lt;/span&gt; 2-5,8-11 oha &lt;span class="nt"&gt;-z&lt;/span&gt; 10s &lt;span class="nt"&gt;-c&lt;/span&gt; 1000 &lt;span class="nt"&gt;--no-tui&lt;/span&gt; http://localhost:3001/todos
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the load generator shares cores with the server, you end up measuring the load generator.&lt;/p&gt;

&lt;p&gt;For each framework, &lt;code&gt;bench.sh&lt;/code&gt; adds 100 todos, warms up for 3 seconds, then runs &lt;code&gt;GET /todos&lt;/code&gt; at 50, 100, 500 and 1000 connections and finally &lt;code&gt;POST /todos&lt;/code&gt;, 10 seconds each. After every run it reads &lt;code&gt;memory.peak&lt;/code&gt; from the container's cgroup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1 CPU, 1 GB RAM&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;actix-web&lt;/th&gt;
&lt;th&gt;axum&lt;/th&gt;
&lt;th&gt;Rocket&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GET req/s, 100 connections&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;67,205&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;47,328&lt;/td&gt;
&lt;td&gt;33,792&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GET req/s, 1000 connections&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;58,279&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;44,788&lt;/td&gt;
&lt;td&gt;31,459&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GET p99, 1000 connections&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;19 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;156 ms&lt;/td&gt;
&lt;td&gt;172 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST req/s, 100 connections&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;123,745&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;113,489&lt;/td&gt;
&lt;td&gt;70,511&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idle RAM&lt;/td&gt;
&lt;td&gt;1.4 MB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.9 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.0 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak RAM, 1000 connections&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;28 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;37 MB&lt;/td&gt;
&lt;td&gt;38 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;2 CPUs, 1 GB RAM&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;actix-web&lt;/th&gt;
&lt;th&gt;axum&lt;/th&gt;
&lt;th&gt;Rocket&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GET req/s, 100 connections&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;128,872&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;80,511&lt;/td&gt;
&lt;td&gt;63,736&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GET req/s, 1000 connections&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;109,103&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;65,114&lt;/td&gt;
&lt;td&gt;57,171&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GET p99, 1000 connections&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;56 ms&lt;/td&gt;
&lt;td&gt;77 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST req/s, 100 connections&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;236,629&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;217,888&lt;/td&gt;
&lt;td&gt;133,975&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No errors and no out-of-memory kills in any run. The full table, including p50 latency for every run, is in the &lt;a href="https://github.com/2u841r/actix-vs-axum-vs-rocket#results" rel="noopener noreferrer"&gt;repo README&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the numbers say
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;actix wins on throughput.&lt;/strong&gt; It did about 1.4x what axum did and about 2x Rocket. It also scaled best from one core to two.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The latency story is more interesting.&lt;/strong&gt; At 1000 connections on one core, axum's median was 5 ms against actix's 17 ms, but axum's p99 was 156 ms against actix's 19 ms. axum and Rocket serve most requests quickly and make a few wait a long time. actix serves everyone at about the same speed. If you care about the slowest 1% of your users, that matters more than the req/s number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RAM doesn't matter at this scale.&lt;/strong&gt; All three idle at about 1 MB and stay under 50 MB holding 1000 open connections. A 1 GB server runs out of CPU long before it runs out of memory. The POST runs did reach 180 to 360 MB, but that was the HashMap holding over a million todos after 10 seconds, not framework overhead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rocket is "slow" only next to the other two.&lt;/strong&gt; 31k requests per second on a single core is about 2.7 billion requests a day. No app I run gets anywhere near that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round two: AWS, over a real network
&lt;/h2&gt;

&lt;p&gt;The laptop run had one obvious hole: the load generator and the server shared a machine and talked over localhost. So I ran it again on AWS in &lt;code&gt;ap-south-1&lt;/code&gt; (Mumbai), with two spot instances:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Instance&lt;/th&gt;
&lt;th&gt;vCPU&lt;/th&gt;
&lt;th&gt;RAM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Server&lt;/td&gt;
&lt;td&gt;&lt;code&gt;c7i.xlarge&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4 (2 physical cores)&lt;/td&gt;
&lt;td&gt;8 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load generator&lt;/td&gt;
&lt;td&gt;&lt;code&gt;c7i.2xlarge&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;16 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both ran Amazon Linux 2023, in the same AZ and a cluster placement group, talking over private IPs. I didn't pick &lt;code&gt;c7i.large&lt;/code&gt; for the server, because its 2 vCPUs are two hyperthreads of &lt;strong&gt;one&lt;/strong&gt; core, and the 2 CPU test would have meant something different from the laptop. On the &lt;code&gt;c7i.xlarge&lt;/code&gt;, vCPUs 0/2 and 1/3 are hyperthread pairs, so the server got &lt;code&gt;0&lt;/code&gt; or &lt;code&gt;0,1&lt;/code&gt;, with 2 and 3 left idle. The container still had the same 1 GB cap.&lt;/p&gt;

&lt;p&gt;There was no SSH. Both boxes were driven with SSM Run Command, and the security group only let the load generator reach ports 3001 to 3003. The whole thing, Docker builds included, took about 20 minutes and cost well under a dollar at spot prices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1 CPU, 1 GB RAM, on AWS&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;actix-web&lt;/th&gt;
&lt;th&gt;axum&lt;/th&gt;
&lt;th&gt;Rocket&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GET req/s, 100 connections&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;66,252&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;47,784&lt;/td&gt;
&lt;td&gt;40,390&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GET req/s, 1000 connections&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;63,885&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;42,090&lt;/td&gt;
&lt;td&gt;36,304&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GET p99, 1000 connections&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;16 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;142 ms&lt;/td&gt;
&lt;td&gt;165 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST req/s, 100 connections&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;171,394&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;150,572&lt;/td&gt;
&lt;td&gt;73,327&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;2 CPUs, 1 GB RAM, on AWS&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;actix-web&lt;/th&gt;
&lt;th&gt;axum&lt;/th&gt;
&lt;th&gt;Rocket&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GET req/s, 100 connections&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;111,959&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;79,327&lt;/td&gt;
&lt;td&gt;61,285&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GET req/s, 1000 connections&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;107,731&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;43,867&lt;/td&gt;
&lt;td&gt;58,774&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GET p99, 1000 connections&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;65 ms&lt;/td&gt;
&lt;td&gt;75 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST req/s, 100 connections&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;286,742&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;228,708&lt;/td&gt;
&lt;td&gt;113,680&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On one core, the cloud matched the laptop almost exactly: actix 66k vs 67k, axum 48k vs 47k. Rocket did a bit better on AWS, 40k vs 34k. POST was faster across the board, likely because the load generator had a whole machine to itself.&lt;/p&gt;

&lt;p&gt;The surprise was &lt;strong&gt;axum on 2 CPUs at high connection counts&lt;/strong&gt;. In the main run it fell to about 44k req/s at 1000 connections, below Rocket. I didn't trust that, so I ran it twice more while the instances were still up:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;axum, 2 CPUs&lt;/th&gt;
&lt;th&gt;c=100&lt;/th&gt;
&lt;th&gt;c=500&lt;/th&gt;
&lt;th&gt;c=1000&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Run 1&lt;/td&gt;
&lt;td&gt;79,327&lt;/td&gt;
&lt;td&gt;44,231&lt;/td&gt;
&lt;td&gt;43,867&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run 2&lt;/td&gt;
&lt;td&gt;80,642&lt;/td&gt;
&lt;td&gt;76,913&lt;/td&gt;
&lt;td&gt;43,217&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run 3&lt;/td&gt;
&lt;td&gt;80,250&lt;/td&gt;
&lt;td&gt;81,166&lt;/td&gt;
&lt;td&gt;76,492&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It lands at either about 44k or about 78k, never in between. actix and Rocket stayed within a few percent in every run. I haven't found the cause yet, but it's a good reminder that one run can lie, and that a stable framework is worth something on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The laptop numbers are a single run. CPU boost and heat move numbers by 5 to 10%, so don't read anything into small gaps.&lt;/li&gt;
&lt;li&gt;The laptop run is over localhost. The AWS run below fixes that.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /todos&lt;/code&gt; returns 100 todos, about 8 KB of JSON. That measures cloning and serde as much as routing.&lt;/li&gt;
&lt;li&gt;The AWS run is one region and one instance family. Graviton (Arm) or AMD instances may rank differently.&lt;/li&gt;
&lt;li&gt;A real app talks to a database. As I found in &lt;a href="https://dev.to/blog/latency-is-geography"&gt;latency is geography&lt;/a&gt;, the database round trip will cost far more than any difference between these three frameworks.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  So which one?
&lt;/h2&gt;

&lt;p&gt;On raw numbers, actix-web, both on my laptop and on AWS. In practice, any of them will handle far more than a 1 CPU server will ever be asked to do. I would pick axum for its ecosystem (tower middleware, tokio everywhere) and how little code it takes, and reach for actix when tail latency under heavy load really matters.&lt;/p&gt;

&lt;p&gt;Whichever you pick, don't &lt;code&gt;unwrap()&lt;/code&gt; while holding a lock.&lt;/p&gt;

&lt;p&gt;The code, the Dockerfile and the benchmark script are on GitHub: &lt;a href="https://github.com/2u841r/actix-vs-axum-vs-rocket" rel="noopener noreferrer"&gt;2u841r/actix-vs-axum-vs-rocket&lt;/a&gt;. Run &lt;code&gt;./bench.sh&lt;/code&gt; on your own machine and see whether your numbers match mine.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>webperf</category>
      <category>systemdesign</category>
    </item>
  </channel>
</rss>
