{"id":135,"date":"2026-04-21T13:02:21","date_gmt":"2026-04-21T13:02:21","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/rust-web-frameworks-2026-axum-actix-rocket\/"},"modified":"2026-04-21T13:02:21","modified_gmt":"2026-04-21T13:02:21","slug":"rust-web-frameworks-2026-axum-actix-rocket","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/rust-web-frameworks-2026-axum-actix-rocket\/","title":{"rendered":"Rust web frameworks in 2026: axum, actix-web, and Rocket in practice"},"content":{"rendered":"<p>Honest opener: I spent two days last month picking between axum and actix-web for a new internal service. I had opinions going in, and somewhat different opinions coming out. This is the write-up I wish I&rsquo;d had before I started.<\/p>\n<p>I&rsquo;ve shipped Rust to production four times now, three of those were web services. Every time I sit down to pick a framework, I think the decision will be easy. Every time I end up re-reading docs and re-running benchmarks anyway. So here&rsquo;s where I&rsquo;ve landed in 2026, what I&rsquo;d actually run on a fresh project, and the cases where I&rsquo;d reach for something else.<\/p>\n<p>Short version for the impatient: axum for almost everything, actix-web when you need its actor model or have battle-tested middleware you don&rsquo;t want to rewrite, Rocket for small services where developer ergonomics matter more than the rest of the ecosystem.<\/p>\n<h2 id=\"the-short-version-and-when-each-one-wins\">The short version (and when each one wins)<\/h2>\n<p>I keep coming back to this table in my head, so here it is up front.<\/p>\n<table>\n<thead>\n<tr>\n<th>Framework<\/th>\n<th>Reach for it when<\/th>\n<th>Avoid when<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>axum<\/td>\n<td>Default. Tower-flavored middleware, tonic gRPC, the broader tokio ecosystem<\/td>\n<td>You want batteries-included routing-by-macro<\/td>\n<\/tr>\n<tr>\n<td>actix-web<\/td>\n<td>CPU-bound handlers, the actor model genuinely fits, you have existing actix middleware<\/td>\n<td>Your team already lives in plain tokio and finds actor lifetimes confusing<\/td>\n<\/tr>\n<tr>\n<td>Rocket<\/td>\n<td>Small services, side projects, teaching Rust web dev<\/td>\n<td>Streaming, gRPC, or any need for the wider tower ecosystem<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>That&rsquo;s most of the post in three rows. Keep reading if you want code and the reasoning behind each pick.<\/p>\n<h2 id=\"why-axum-ended-up-being-my-default\">Why axum ended up being my default<\/h2>\n<p>axum wins for me because it composes with the rest of the tokio world. It uses <code>tower::Service<\/code> everywhere, which means middleware written for tonic, hyper, reqwest, or a raw tower stack drops in without a translation layer. That&rsquo;s not a nice-to-have. That&rsquo;s the whole pitch.<\/p>\n<p>Here&rsquo;s a real-ish example. You want a service with tracing, timeouts, compression, and an auth middleware. In axum that looks like:<\/p>\n<pre><code class=\"language-rust\">use axum::{routing::get, Router, middleware};\nuse tower_http::{trace::TraceLayer, timeout::TimeoutLayer, compression::CompressionLayer};\nuse std::time::Duration;\n\n#[tokio::main]\nasync fn main() {\n    let app = Router::new()\n        .route(&quot;\/health&quot;, get(|| async { &quot;ok&quot; }))\n        .route(&quot;\/api\/users&quot;, get(list_users).post(create_user))\n        .layer(TraceLayer::new_for_http())\n        .layer(TimeoutLayer::new(Duration::from_secs(10)))\n        .layer(CompressionLayer::new())\n        .layer(middleware::from_fn(auth_middleware));\n\n    let listener = tokio::net::TcpListener::bind(&quot;0.0.0.0:3000&quot;).await.unwrap();\n    axum::serve(listener, app).await.unwrap();\n}\n<\/code><\/pre>\n<p>The layers stack because every one of them is a tower service. Swapping in a custom rate limiter or circuit breaker is <code>.layer(MyLayer::new())<\/code>. I&rsquo;ve used that property to share a middleware stack between gRPC services (via tonic) and HTTP services with the same observability and auth code.<\/p>\n<p>The extractor system is the other thing I like. Handlers look like normal async functions with typed arguments, and axum figures out how to pull each one from the request:<\/p>\n<pre><code class=\"language-rust\">use axum::{extract::{State, Path, Json}, http::StatusCode};\n\nasync fn create_user(\n    State(db): State&lt;Pool&gt;,\n    Path(org_id): Path&lt;u64&gt;,\n    Json(payload): Json&lt;NewUser&gt;,\n) -&gt; Result&lt;Json&lt;User&gt;, StatusCode&gt; {\n    let user = db.insert_user(org_id, payload).await\n        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;\n    Ok(Json(user))\n}\n<\/code><\/pre>\n<p>No procedural macros, no opaque trait objects. The compiler tells you when a handler signature is wrong. I&rsquo;ve had a junior engineer land axum PRs on day three of their Rust career, which I cannot say honestly about actix.<\/p>\n<h2 id=\"actix-web-the-workhorse-that-still-earns-its-keep\">actix-web: the workhorse that still earns its keep<\/h2>\n<p>I ran actix-web in production for about eighteen months before axum got stable enough for me. It&rsquo;s still faster than axum on a lot of small-JSON benchmarks, and the actor model is genuinely useful when you have stateful work that needs to be serialized across requests, like a single-writer queue or a per-tenant rate limiter that needs strict ordering.<\/p>\n<p>A minimal actix-web handler is recognizable if you&rsquo;ve used axum:<\/p>\n<pre><code class=\"language-rust\">use actix_web::{web, App, HttpServer, HttpResponse};\n\nasync fn create_user(\n    db: web::Data&lt;Pool&gt;,\n    path: web::Path&lt;u64&gt;,\n    payload: web::Json&lt;NewUser&gt;,\n) -&gt; actix_web::Result&lt;HttpResponse&gt; {\n    let user = db.insert_user(path.into_inner(), payload.into_inner()).await\n        .map_err(|e| actix_web::error::ErrorInternalServerError(e))?;\n    Ok(HttpResponse::Ok().json(user))\n}\n\n#[actix_web::main]\nasync fn main() -&gt; std::io::Result&lt;()&gt; {\n    HttpServer::new(|| {\n        App::new()\n            .app_data(web::Data::new(pool()))\n            .route(&quot;\/api\/users\/{org_id}&quot;, web::post().to(create_user))\n    })\n    .bind(&quot;0.0.0.0:3000&quot;)?\n    .run()\n    .await\n}\n<\/code><\/pre>\n<p>What bites you with actix-web is the runtime story. actix-web spins up its own tokio runtime variant by default, and mixing it with crates that assume a standard multi-threaded tokio takes more care than it should. I once spent an afternoon chasing a &ldquo;runtime mismatch&rdquo; panic that turned out to be actix&rsquo;s current-thread runtime stepping on a Postgres pool that expected multi-threaded tokio. That kind of bug is hard because you don&rsquo;t even know which layer to suspect first.<\/p>\n<p>The other thing: actix&rsquo;s actor model is a separate mental model on top of async\/await. Teams that already know tokio find axum easier to pick up. Teams coming from an actor background (Akka, Erlang, that crowd) will find actix natural, sometimes more natural than axum.<\/p>\n<h2 id=\"rocket-the-ergonomics-king-with-one-big-trade-off\">Rocket: the ergonomics king with one big trade-off<\/h2>\n<p>Rocket is the framework I recommend when someone is learning Rust web dev and I don&rsquo;t want them to bounce off the language. It is macro-heavy on purpose, and the developer experience is closer to Flask or Django than anything else in the Rust ecosystem:<\/p>\n<pre><code class=\"language-rust\">#[macro_use] extern crate rocket;\n\nuse rocket::serde::{Deserialize, json::Json};\n\n#[derive(Deserialize)]\n#[serde(crate = &quot;rocket::serde&quot;)]\nstruct NewUser { name: String, email: String }\n\n#[post(&quot;\/users\/&lt;org_id&gt;&quot;, data = &quot;&lt;payload&gt;&quot;)]\nasync fn create_user(org_id: u64, payload: Json&lt;NewUser&gt;) -&gt; Json&lt;User&gt; {\n    \/\/ ... call into a service layer here ...\n}\n\n#[launch]\nfn rocket() -&gt; _ {\n    rocket::build().mount(&quot;\/api&quot;, routes![create_user])\n}\n<\/code><\/pre>\n<p>Clean, right? That&rsquo;s the whole appeal. The catch is the ecosystem: Rocket doesn&rsquo;t compose with tower, and its middleware story (called &ldquo;fairings&rdquo;) is its own thing. If the library you need is tower-flavored, you&rsquo;ll fight it. For a side project where I control all the moving parts, Rocket is a nice place to live. For anything I expect to grow past five engineers, I don&rsquo;t pick it.<\/p>\n<h2 id=\"middleware-and-extractors-where-the-daily-pain-lives\">Middleware and extractors: where the daily pain lives<\/h2>\n<p>Benchmarks aren&rsquo;t where these frameworks really differ. I don&rsquo;t care if axum handles 180k req\/s while actix handles 220k when my Postgres is doing 800 req\/s on a good day. What matters is what happens the tenth time I add a cross-cutting concern.<\/p>\n<p>In axum, middleware is tower. If you can write a <code>Service<\/code>, you can plug it into axum, hyper, or tonic. There&rsquo;s a real learning curve to tower itself, but you only pay it once. The <a href=\"https:\/\/docs.rs\/tower\/latest\/tower\/\" rel=\"nofollow noopener\" target=\"_blank\">tower documentation<\/a> is dense but worth it, and what you learn there transfers to everything else in that ecosystem.<\/p>\n<p>In actix-web, middleware is its own trait hierarchy with <code>Transform<\/code> and <code>Service<\/code> impls that aren&rsquo;t the same <code>Service<\/code> as tower&rsquo;s. The <a href=\"https:\/\/actix.rs\/docs\/middleware\/\" rel=\"nofollow noopener\" target=\"_blank\">actix-web middleware guide<\/a> is solid, but the concepts don&rsquo;t port. If you also write tonic services or wrap reqwest in a tower stack, you&rsquo;re maintaining two parallel mental models.<\/p>\n<p>In Rocket, middleware is fairings, which are simpler but less powerful. There&rsquo;s no streaming middleware story and no per-route layering the way you&rsquo;d write it in axum.<\/p>\n<p>For a service that will pick up new cross-cutting concerns over time (auth, rate limiting, metrics, tracing, request IDs, tenant isolation), axum&rsquo;s tower story is the one that keeps paying dividends.<\/p>\n<h2 id=\"tooling-and-the-things-benchmarks-wont-tell-you\">Tooling and the things benchmarks won&rsquo;t tell you<\/h2>\n<p>Three things I wish someone had told me before my first Rust web service:<\/p>\n<p>First, tracing is not optional. Whichever framework you pick, wire up <code>tracing<\/code> plus <code>tracing-subscriber<\/code> plus an OpenTelemetry exporter on day one. axum&rsquo;s <code>tower-http::TraceLayer<\/code> makes it almost trivial. actix has <code>actix-web-opentelemetry<\/code>. Rocket has ad-hoc community integrations. Budget a day for this. You&rsquo;ll save a week the first time prod misbehaves.<\/p>\n<p>Second, error types compound. Every handler can return its own error, and once you have twenty of them you&rsquo;ll wish you&rsquo;d built a unified application error from the start. The pattern I keep reaching for is the one I wrote about in <a href=\"https:\/\/abrarqasim.com\/blog\/go-error-handling-patterns-that-actually-help\" rel=\"noopener\">Go-style error handling patterns that actually help<\/a>: tag errors with context, return them as values, and centralize the conversion to HTTP responses in one place. The advice translates surprisingly cleanly to Rust.<\/p>\n<p>Third, check whether your database driver is runtime-aware. <code>sqlx<\/code> is happy on tokio. Some Postgres clients assume async-std. A handful of older Redis crates pin specific runtime versions. I&rsquo;ve been burned by every one of these and the symptoms always look like &ldquo;my service hangs during startup&rdquo; with no useful log line.<\/p>\n<p>If you want to compare notes on what I&rsquo;ve shipped in Rust and what stack I picked for each project, I keep that on my <a href=\"https:\/\/abrarqasim.com\/work\" rel=\"noopener\">work page<\/a>. Happy to swap stories if your constraints look different.<\/p>\n<h2 id=\"what-id-actually-run-this-week\">What I&rsquo;d actually run this week<\/h2>\n<p>If you&rsquo;re starting a fresh Rust web service in April 2026, my honest recommendation is to start with axum, use <code>tower-http<\/code> for middleware, <code>sqlx<\/code> for your database, and <code>tracing<\/code> plus an OpenTelemetry exporter for observability. You can read the <a href=\"https:\/\/docs.rs\/axum\/latest\/axum\/\" rel=\"nofollow noopener\" target=\"_blank\">axum docs<\/a> end-to-end in an afternoon and have a working service the same day. That stack composes, compiles, and scales further than most teams ever need.<\/p>\n<p>If you need to squeeze every last request out of a single box, benchmark actix-web against axum on your actual workload. Don&rsquo;t trust TechEmpower numbers in isolation. The performance gap is smaller in 2026 than it was two years ago, and middleware composition costs more than raw routing in any service worth measuring.<\/p>\n<p>If you&rsquo;re teaching someone Rust web dev from scratch, start them on Rocket and migrate them to axum once they understand the extractor pattern. The conceptual jump is small and Rocket gives them dopamine hits earlier in the learning curve.<\/p>\n<p>One concrete thing to do this week: spin up a new repo, add <code>tokio<\/code>, <code>axum<\/code>, <code>tower-http<\/code>, and <code>tracing-subscriber<\/code>, and ship a single <code>\/health<\/code> endpoint that returns <code>ok<\/code>. Then bolt on tracing. Then add a database. In that order. The framework you picked matters less than the fact that you got past the empty <code>Cargo.toml<\/code>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I picked axum for a new Rust service last month. Here&#8217;s what I learned comparing axum, actix-web, and Rocket in 2026, with code and when I&#8217;d switch.<\/p>\n","protected":false},"author":2,"featured_media":134,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"I picked axum for a new Rust service last month. Here's what I learned comparing axum, actix-web, and Rocket in 2026, with code and when I'd switch.","rank_math_focus_keyword":"rust web framework","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[45],"tags":[118,117,49,119,64,120,121],"class_list":["post-135","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-programming","tag-actix-web","tag-axum","tag-backend","tag-rocket","tag-rust","tag-rust-web-development","tag-web-frameworks"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/135","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/comments?post=135"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/135\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/134"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=135"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=135"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=135"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}