Actix-web 1.0 – A small, pragmatic, and fast web framework for Rust
docs.rs
docs.rs
This is a major milestone for the entire Rust community because we now have the first web framework written in stable Rust with an architecture that a credible author has deemed worthy of maintaining backwards compatibility for and a code base mature enough to have earned a 1.0 designation. This is real progress.
The architecture of actix-web 1.0 is very different from that of 0.7. In many respects, it was a rewrite that began last Summer. The architecture is no longer based on an actor paradigm but rather one of Services, largely inspired by Eriksen et al's finagle work [1] adopted at Twitter. This service architecture is accessible through a library known as actix-net. Essentially, actix-web is a web-based actix-net server. If actix-web were bitcoin, actix-net would be its blockchain. Because of this, actix-net may be even more significant to the broader Rust community. The actor models can still be imported and used but no longer act as the default mechanisms driving the server.
Regarding performance, according to the Tech Empower benchmarks that evaluate hundreds of web frameworks across all major languages, actix-web is top-ranking [2] and first in a few benches. The last benchmark represents a beta version of actix-web from last month and results may have slightly improved with 1.0. actix-web is very modular, allowing programmers to only use what is needed and no more.
In terms of usability, actix-web 1.0 api is far easier to approach than 0.7. Running blocking calls against a database effortlessly flows within combinators now where as before one had to implement a lot of SyncActor boilerplate. Endpoint resource registration can now either be explicitly registered within an App instance or with a new set of proc macros, but routes still need to be registered manually. The new testing api is far easier to work with for unit or integration tests.
A single person wrote two very different platforms in order to get to where it is today. I believe that future progress requires community participation at all levels. This is a great time for system programmers to take a deep dive and learn, from the network-level up, how to build a high-performing, flexible server. Write about it. Talk about it at meetups and conferences. Pay forward.
Thanks, Nikolay, for your hard work and sacrifice.
[1] https://monkey.org/~marius/funsrv.pdf
[2] https://www.techempower.com/benchmarks/#section=test&runid=9...
I'm also interested in any alternatives for implementing an actor-based application in Rust, if any.
If the actix library suits your needs, why look elsewhere? Develop expertise and maintain it. If another actor project exists, the same risk of continuity applies.
Now that Actix isn't used in Actix-web anymore, I would expect two alternatives: it either continues to be developed at a fast pace (with frequent breaking changes) or it will settle it in current state, due to the lack of interest from its main author (with possible maintenance concerns, but with no more breaking changes). But I don't see how both could happen at the same time…
1. What is the reason for rewriting the framework to abandon actors in favor of services?
2. In the Tech Empower benchmarks, what's the difference between actix-core and actix-pg? What caused the difference in performance between the 2?
3. What's your experience with using actix-web in production? How does it compare to other frameworks you've used?
Thanx
[1]. https://github.com/TechEmpower/FrameworkBenchmarks/tree/mast...
[2]. https://github.com/TechEmpower/FrameworkBenchmarks/blob/mast...
[3]. https://github.com/TechEmpower/FrameworkBenchmarks/blob/mast...
actix-pg: 27.1 ms
actix-core: 201.1 ms
Is that to be expected?
[1] https://www.techempower.com/benchmarks/#section=test&runid=9...
Strange to see that actix-diesel dropped in performance for this run though
There are also plenty of drawbacks though: the extensive type system hacking can lead to very confusing compiler errors, it doesn't work well when you need to sometimes fall back to more dynamic db handling, and it doesn't handle large tables well. (very long compile times etc), and the documentation is quite sparse.
It's a robust solution for greenfield projects where you can design the schema partially with diesel in mind, but I wouldn't recommend it for existing DBs or projects that you know will grow large (eg 100+ column tables etc).
By large tables you mean a large number of columns, not a large number of rows, right?
Fun fact: The amount of traits implementations that need to be generated to support tables with 128 columns actually makes diesel an interest benchmark of the Rust compiler.
It has to be said though, that compared to other ecosystems, less focus is put on ORMs, and more focus is put on the specific database crates (e.g. rust-postgres), and how to make them easy to use without ORMs.
[0]: https://diesel.rs
q = select("table")
q = q.where_greater_than("col", 5)
if(x) { q = q.where_equal("col2", true) }It appears this example I found is a little dated (using .8 and actors).
Thanks!
Hope this helps!
It's been relatively easy to use and flexible enough to integrate with the rest of Cloudflare's stack. Feels vaguely similar to Node's express. Along with Rust it's a great fit for a service that needs to have high performance, tight memory usage with very low risk of leaks, integration with existing libraries, and a good security story.
Oh, that's really interesting. I actually used it for exactly the same purpose, but I was a bit disappointed by the image[1] crate for the image-resizing itself: it was several times slower than imagemagick (it was less the case on my decktop, when compiled with `target-cpu=native`, but on my low-end server many SIMD instructions weren't available and auto-vectorized code wasn't that efficient) and the image quality was also way poorer. Did you use that crate for the image processing ? Or imagemagick ? Or something you wrote internally ?
Also, I'm not sure I would trust imagemagick enough anymore to put it on a web-facing server, since its security track record doesn't smell that good [1].
But I was still disappointed by the poor performance of the `image` library because Rust has shown in many areas that it has potential to write code which is as fast as C, and in this case it didn't deliver.
[1]: https://www.cvedetails.com/vulnerability-list.php?vendor_id=...
Do you know how it compares?
Couldn’t be happier with it. It’s super fast and stable and does it’s job really well. We run three machines for it and it didn’t manage to go past 10% CPU usage yet.
Code is here for the curious: https://github.com/getsentry/symbolicator
Thank you! I am a really satisfied user of Rust and Actix Web! I hope I can retire my programming career just using Rust lol.
Here's the simple middleware examples:
0.7: https://github.com/actix/examples/blob/0.7/middleware/src/si...
1.0: https://github.com/actix/examples/blob/master/middleware/src...
Could a simple Middleware trait be added that would implement both new traits for us?
[1]: actix_web::middleware::Middleware
Congratulations to the authors/contributors of actix-web on the 1.0 release!
The biggest hurdles for me (I'm still learning) compared to JS is the borrow checker, lifetimes, and strings (they are handled very precisely in Rust).
The benefits are huge though. My time coding in JS feels like 50% coding & writing tests, 40% debugging my app, 10% reading documentation. The "debugging" portion can sometimes be painful when fixing race conditions. Rust on the other hand, feels like 30% reading documentation, 30% coding, 30% getting my code to compile, and the other 10% debugging/testing my app. For the most part, if your code compiles and your logic is correct, your app is guaranteed to run the way you expect it to.
The "high level" stuff baked into Rust makes it great for "low level" tasks, notably imo:
- Tests (and micro benchmarks, with nightly) alongside code, without dependencies or build system hackery.
- An incredibly powerful macro system, supplanting a lot of the templated code generation I've done in C++ which is a nightmare. Not that proc macros are perfect (yet) but at least they're legible.
- If you've ever tried to do away with dynamic dispatch via templates in place of inheritance, then Rust's generics with trait bounds are an absolute godsend
- A gosh darn dependency solution (now with custom repositories on stable!) makes dependency hell is less hellish
That's the theory anyway. I haven't gotten any data from people trying that on real-world apps, though. There could be non-obvious problems with it.
https://www.php.net/manual/en/features.gc.collecting-cycles....
It seems like people who often use Rust's Rc and Arc will long for some way to implement generic cyclic garbage collection. I don't yet see a way (other than unsafe code), but I may be missing something.
Low-level programming is possible in Rust, as it is in many high-level languages, but it is discouraged. Use of "unsafe" is rightfully met with scepticism.
C++ is also a high level language, but does not enforce use of high-level safety abstractions by default. Web programming in C/C++ is possible, and frameworks exist to do it, but memory safety (security) concerns make it undesirable.
It's not like the C/C++ specs are particularly precise. I think people tend to have a mental model of specs as equivalent to formal semantics, which they very much are not.
And I think it’s a bit more subtle than you’re giving credit for.
If you’ve noticed that lately there are a lot more pieces of systems software built in the architecture of “a daemon plus a client executable that talks to it over a control-channel Unix domain socket, but where that same control-channel [with different auth logic] can also be exposed over the Internet” — that’s thanks mostly to people writing things in Go, and Go making that architecture easy to set up (with the simplest option being a gRPC-protocol connection, but a RESTful HTTP service layer being only a little bit more work.)
Now it’s easy to set up such a RESTful service layer for your daemon in Rust, too!
At least I would only reach out for Rust at the moment for performance critical code, and then don’t leave easy optimizations on the table due to lifetimes.
I do agree though that the tooling is very immature compared to other ecosystems, and Rust is generally quite boilerplate heavy and slower to develop.
It can still be a good choice if you need really high performance/low resource consumption - or you are going for high correctness and robust error handling.
But for most web backends Rust won't make much sense.
(I say this as someone who even writes web frontends with Rust via wasm, where it is even less optimal - mainly due to the high compile times)
With Rust I don't think you have to choose.
Once you "get" the borrow checker and understand the ownership model Rust becomes very productive and feels like a high level language. There's a little noise in the syntax but what you get in return is so worth it. There are quite a bit of high level features to take advantage of that "compile away" with so called zero-cost abstractions.
It's a legit low level language in the sense that you can write a legit OS. No GC, guaranteed memory safety. It's bonkers.
I believe it is blurring the lines of what it means to be high/low level.
The couple of times I've checked it's been fairly up to date and helpful for understanding what's out there.
@app.route("/<name>/<id>/index.html")
def index(name, id):
return form("Hello {}! id:{}", name, id)
This has the advantages that:1. the endpoint "/<name>/<id>/index.html" goes in the code right above the function that implements it.
2. the variables are called `name` and `id` and not `info.0` and `info.1` which is a less informative naming convention.
#[get("/resource1/{name}/index.html")]
fn index(req: HttpRequest, name: web::Path<String>) -> String {
println!("REQ: {:?}", req);
format!("Hello: {}!\r\n", name)
}
see https://github.com/actix/actix-web/blob/e399e01a22b8a848ecbb...I also had no idea about this macro until I was digging through documentation for something else.
[derive(Deserialize)]
struct HelloWorldParams {
id: String,
name: String,
}
fn index(info: web::Path<HelloWorldParams>) -> impl Responder {
format!("Hello {}! id:{}", info.name, info.id)
}In general, a lot of things that are language features in higher-level languages, can and are implemented as libraries in Rust. It's possible that this will make it into the standard library at some point.
format!("{a} {c} {b}", a="a", b='b', c=3)
In real world projects, you will often transform at interpolation site which is better with Rust's format! syntax since it won't clutter the inline string. Not something I think is worth bike-shedding further. a = "a"
b = 'b'
c = 3
"{a} {c} {b}"
Your concern is addressed, with real interpolations, already.For debugging, the `dbg` macro is the way to go, so you can skip string interpolation for that entirely (see https://doc.rust-lang.org/std/macro.dbg.html for details).
And when you do want to use string interpolation, you can always grab a crate such as interpolate or any of the others. (https://crates.io/search?q=interpolate). No need to change how the language works when you can download or build a library that suits you.
I think the biggest piece of added complexity would be that String would need to be a lang item, giving the compiler special knowledge about it. Currently Vec and String are plain library types, so conceptually this would be a pretty big change.
One downside: if you move from interpolating a simple variable name to an expression, then you have to move it out-of-line from the string again (or you have to support interpolating arbitrary expressions into strings, which gets really painful and makes it hard for editors to check/highlight/etc).
You can now deploy an Actix web app in production in just a few minutes.
https://render.com/docs/deploy-actix-todo
A Rust user testimonial: https://twitter.com/sebasporto/status/1136032748894736384
use actix_web::{web, App, HttpServer, Responder};
fn index(info: web::Path<(u32, String)>) -> impl Responder {
format!("Hello {}! id:{}", info.1, info.0)
}
fn main() -> std::io::Result<()> {
HttpServer::new(
|| App::new().service(
web::resource("/{id}/{name}/index.html").to(index)))
.bind("127.0.0.1:8080")?
.run()
}
How do you know the HTTP method your path is called with? web::resource("")
.route(web::get().to_async(products::get_products))
.route(web::post().to_async(products::add_product)) use actix_web::{web, App, HttpServer, HttpRequest, Responder};
fn index(req: HttpRequest, info: web::Path<(u32, String)>) -> impl Responder {
format!("[{}] Hello {}! id:{}", req.method(), info.1, info.0)
} App.new().service(
web::resource("/welcome")
.route(web::get().to(welcome))
.route(web::post().to(post_handler))Would you also think authorization is counter-intuitive when you're using nightly?
Your concern is that the instability increases the risk of an insecurity, but that is more like arguing that a finger prick on an otherwise healthy patient is irrelevant because that patient could become mortally wounded in the future. Hospitals treat the patients they have, they don't say "ah, you could die in a car accident anyway, might as well not help anybody."
I want to also say rocket.rs is amazing, and I don't at all fault all the amazing things the contributors are doing. It's a great project, with just superb features and support. I think they should keep going and eventually rocket.rs will be an amazing choice for web developers who want all the benefits of rust. Yes of course they should care about application security.
What I do fault is people now who are going to run a production application with a binary built with a nightly compiler. They are doing a disservice to their users, to their fellow coworkers, and any investors if they have any. There's no sane reason for pitching rocket.rs using experimental features of rust as a part of any project on which people's jobs and users' data depend on.
I've been extremely satisfied with its performance and ergonomic abstractions over HTTP and async Rust that actix-web offers. And like others have mentioned, the author and other contributors provided me with some good, practical answers to a few questions I had.
I was a bit disappointed because I really wanted to use rust as a small-footprint app server :(