Fast, Safe, and Complete(ish) Web Service in Rust
brandur.org
brandur.org
> If it’s possible to build more reliable systems with programming languages with stricter constraints, what about languages with the strongest constraints? I’ve skewed all the way to the far end of the spectrum and have been building a web service in Rust
Bit overselling it, that introduction would work better for something like ATS or possibly Idris. Rust has its strictness, but it's not exactly "you must prove this recursion terminates"-strict
> Synchronous operations aren’t as fast as a purely asynchronous approach
This is misleading. Synchronous sequences are generally faster than asynchronous ones because yielding and getting scheduled back has a non-zero overhead (this is regularly re-discovered, most recently by the developers of "better-sqlite": https://news.ycombinator.com/item?id=16616374).
Async operations are a way to increase concurrency when your synchronous sequence would just be waiting with its thumb up its ass (e.g. on a network buffer filling or on a file read completing), so you can provide more service over the same wallclock time by (hopefully) always using CPU time.
Async is generally associated with lower latency.
Can you expound?
In the threaded code, you need to get a window of time and memory. In my Java, I have a max heap size of 8gb and I can maybe create up to 8k threads. When making 20000 DB queries only 8k is being executed where most of these are waiting for the query to complete or CPU time.
I know that you can have millions of M:N/green threads but these are different. In any case, you need to compete for memory and CPU time to execute.
Since not many people are programming in Haskell and BEAM languages my simplifications still hold.
Isn't that vmem? Unless it also writes to the entirety of that 1MB before starting the thread, it's not actually committed by default on most unices.
public class Test {
volatile static boolean stop = false;
static AtomicInteger counter = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[100000];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
counter.incrementAndGet();
while (!stop) {
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
}
}
});
}
for (Thread thread : threads) {
thread.start();
}
while (counter.get() < threads.length) {
Thread.sleep(1000);
}
System.out.println("Threads are running");
Thread.sleep(10000);
stop = true;
for (Thread thread : threads) {
thread.join();
}
}
}To get a bit pedantic, synchronization introduces overhead. If you've got an 8-core processor running things async/parallel may complete faster since you aren't pinned to one core.
That said, it's much harder to get right since you have to deal not only with thread sync overhead but also cache/pipeline sharing and a bunch of other gnarly things.
Your asynchronous system can be descheduled by the OS on top of the stuff it does on its own if it runs on a preemptive multitasking OS or hypervisor, which more or less all of them do.
> And it cannot ever be faster than asynchronous, because asynchronous doesn't yield.
The asynchronous system yields doubly, it yields to its own userland scheduler/event loop and it's de-scheduled by the preemptive multitasking OS it runs on top of.
Yielding is a thing from the multithreaded world with synchronous APIs. Asynchronous systems don't yield, they are programmed asynchronously. They have no yielding overhead and can never be slower than yielding threads.
If you are arguing for 1:1 kernel threading against M:N threading (coroutines, goroutines, green threads) that's fine, but multithreading implementations have absolutely nothing to do with asynchronous systems.
https://en.wikipedia.org/wiki/Asynchrony_(computer_programmi...
Indeed. I remember being shocked when I first read "Thousands of Threads and Blocking I/O" [0] by Paul Tyma.
After reading the C10k problem [1], I just believed that a big pile of threads would never work and never updated my beliefs. While such an approach is still quite memory limited compared to asynchronous approaches, it scales quite far and can have a very high work rate.
on other hand we should talk about C100k or C1m problems.
This is why actor models are good IMO. They are based on the actual resource model, rather than the developer's mental model. That is, until the developer learns to change his/her mental model.
time_helpers::log_timed(&log.new(o!("step" => "upsert_episodes")), |_log| { }
It's just non-sense:
impl<S: server::State> actix_web::middleware::Middleware<S> for Middleware {
fn start(&self, req: &mut HttpRequest<S>) -> actix_web::Result<Started> {
let log = req.state().log().clone();
req.extensions().insert(Extension(log));
Ok(Started::Done)
}
Rust is a good language but I don't see it any time soon in the web space with that syntax and the lack of mature libraries, plus the time it takes to get anything done. Where I see it is more for super optimized things like serialization and the like as libraries but not as a main program.Also, where do you get that the performance is the same? I didn't see any benchmarks.
https://www.techempower.com/benchmarks/#section=data-r15&hw=...
tbh benchmarks are not that useful, but it shows that GC languages are very fast in the web space and it's even more true when you add client -> server latency in the mix.
That said, actix is particularly new, with room to optimize.
Arenas and lifetimes work well together; the compiler actually has a hierarchy of arenas inside of it, and makes sure thanks to lifetimes that once the shorter-lived ones are freed, they don't get used again, for example.
Here's an example of some Rust code that's even faster and is short and sweet: https://github.com/tokio-rs/tokio-minihttp/blob/master/examp...
(I don't think the TechEmpower benchmarks really mean much, incidentally. They're so simple that they're way too easy to game.)
Some laser-focus on the plaintext benchmark in particular, presumably because it's the easiest to implement and the delightfully high numbers in the results are exciting. But more interesting are the test types that involve round-trips to the database, using an ORM (or similar), and other framework features. For me, the Fortunes test is the most interesting.
Yes, it's "easy" to climb up a few positions and jockey for specific rank order against other similar contenders, but considering specific rank ordering is not a useful way to interpret the results. More useful is to see the orders of magnitude conveyed by the results. And it is not so easy for a low-performance platform (e.g., Ruby) to game its way into the top positions. See, for example, Java, Rust, and Ruby on the Fortunes test:
https://www.techempower.com/benchmarks/#section=data-r15&hw=...
Obviously, no small benchmark can measure the performance characteristics of your application as implemented on multiple frameworks. But these are a proxy, and illustrate the performance landscape your application will exist within. They can reveal the bottlenecks your application will be unable to avoid in framework services such as JSON serialization, object-relational conversion, connection pooling, server-side templates, and so on.
A very good point, and I had overlooked what the parent poster had said!
Rust is clearly part of the top performance category and I would not be surprised if with some work it were to become (momentarily) faster by some small amount than Java, Go, etc. I say "momentarily" here because many of the performance-oriented frameworks and platforms are continuously working on tuning within their stacks, so there is considerable volatility in the specific ordering over time.
For Ruby, most of the expensive parts of shuffling around HTTP requests, fetching data from a DB, and serializing JSON, is done using libraries with native extensions. This means using a lightweight framework and ORM like Sinatra + Sequel can put you within striking distance (~3x slower) than Go + Gin on benchmarks like the TechEmpower benchmarks.
However, if you do something with multiple passes of tight loops over the data before you return it, Go will pull much further ahead.
To answer your question more directly (though I feel the article covers these points), as someone building services in rust, there are a few reason I'd choose Rust over the languages you listed:
a) Largely deterministic performance
Having written services professionally in Java I have seen performance scale linearly with connections, until one day it suddenly spikes massively - why? It was that we crossed some threshold where the GC was suddenly having to do a ton more work, which caused a serious feedback loop - server has to pause, clients don't handle it well, clients hammer server more, server has to pause, etc.
You can have similar issues in Rust but they should be easier to spot (they should be less blow-up-y).
b) Correctness/ Expressiveness (is this a word?)
I find it far, far easier to write correct, expressive code in Rust. For example, I think anyone who has moved from Java 6 to 7 or 8 will admit that Optional is awesome - because it is. In Rust the pattern exposed by Optional is first class, and extends far beyond nullability. This is just one example though.
I find it extremely easy, by comparison to other languages, to write Rust code in a 'type driven' way, and in my experience this is a huge part of (me) writing code with fewer defects.
As the linked article puts it:
> Constraints like a compiler and a discerning type system are tools that help us to find and think about those edges.
I do not think this is just about unfamiliarity. Doing static dispatch with generics + traits does add complexity to type signatures. E.g. consider using dynamic dispatch using trait objects
trait Frobber {
fn frob(&self, something: &OtherTrait);
}
versus static dispatch: trait Frobber {
fn frob<S>(&self, something: &S) where S: OtherTrait;
}
Of course, static dispatch has large benefits (opportunities for inlining, possibly leading to other optimalizations). Rust adds correctness and deterministic performance (besides Rc/Arc misuse). But Rust also has the potential for 'macro/generics soup' and over-abstraction. I think it is a fair criticism of a language. Use sharp tools wisely.(Disclaimer: writing Rust daily for work.)
trait Frobber {
fn frob(&self, something: &impl OtherTrait);
}Fair, but you almost never see things like this with Go, and when you do, it's pretty straightforward to triage since the GC and escape analysis are far simpler. Meanwhile, the performance is comparable.
> I find it far, far easier to write correct, expressive code in Rust.
I know this paragraph was comparing Rust to Java and not to Go, but while Rust is more expressive than Go, man does it take a long time to figure out how Rust wants you to express what you want to express. On the other hand, most things in Go do what you would expect, but sometimes you need to subvert the type system (which is a bummer, but it's worth it to me to sacrifice 1% of type safety for an order of magnitude or more productivity improvement).
The Go GC is not a panacea. It trades lower STW pause times for a huge drop in throughput under GC pressure and simplicity for terrible performance with large, mostly static heaps when compared to a generational collector.
This entire discussion and the original article is worth reading: https://www.reddit.com/r/programming/comments/5fyhjb/golangs...
CockroachDB only works at all because the actual DB is in RocksDB written in C++.
Keep in mind that the HotSpot JVM does escape analysis just as Go does (this is sadly a very common misconception). The difference is that Go relies on escape analysis a lot more than the JVM does, because Go's GC has much slower allocation due to not being generational.
> man does it take a long time to figure out how Rust wants you to express what you want to express.
This goes away with experience. Certainly, most programmers get up to speed with Go faster than they do with Rust. But at this point the Rust language has totally melted away into the background for me.
True, but this is only a part of the story. In Java, every custom type is a reference type. This puts a lot of pressure on the GC because a lot of multiple small objects are created. Go has value types and doesn't suffer from this issue, which is a reason why having a generational GC is not that important for Go.
… but they are allocated in the nursery, which isn't comparable to an allocation in Go (by orders of magnitude). It only makes a difference if you have a long-lived object (which must be tenured) and the difference is noticeable only if the object is small enough to be memcopied multiple times at low cost.
That's why Go is only significantly faster than Java on the Go's marketing slides and not in real life.
In Java, if you allocate an object containing 10 other objects, you have to allocate 11 objects, because you only only have reference types (I know there is ongoing work to introduce value types). This is more work for the allocator, and for the GC. In Go, you allocate only once.
Honestly, I'm not sure about this issue being very significant for most programs in either Go or Java. As a reminder, most C, C++ and Rust programs don't use a bump allocator either and they're doing well.
Indeed. In fact, Go couldn't afford not to have a generational GC without value type and Java would be really slow with Go's GC.
> As a reminder, most C, C++ and Rust programs don't use a bump allocator either and they're doing well.
C, C++ and Rust can't use a bump allocator since you need a compacting GC to do so. But allocations are really expensive in these languages and removing them is often the first step of optimization.
And Java couldn't afford not to have value types without a generational GC ;-)
> C, C++ and Rust can't use a bump allocator since you need a compacting GC to do so. But allocations are really expensive in these languages and removing them is often the first step of optimization.
Agreed. This is why Go programmers use the same optimizations as C, C++ and Rust programmes is such a case (by allocating from a pool or an arena).
Are you aware of cases where what is gained thanks to the bump allocator is lost because of compaction?
Yes, unfortunately in Go allocations are harder to avoid[1] than in C++ or Rust, because Go rely on escape analysis whereas objects must be manually boxed in the other languages.
[1]: https://groups.google.com/forum/#!topic/golang-nuts/Vcgx7hkh...
Not true. You can use exactly the same techniques in C, C++ and Rust. It's called arena based allocation.
Even malloc calls can be very cheap. The Hoard allocator uses bump pointer allocation for malloc into empty pages!
It's actually very common to do this in performance oriented code. You allocate a decently sized temporary chunk of memory at the start of some period of work (say a frame of a game, or something like that) and then most/all temporary allocations are done from that in a bump pointer fashion. None of it get's freed, but in practice this isn't a big problem as long as you allocate a sufficiently large buffer (or handle overflow by allocating a second)
At the end of the period of work, you free the buffer. In the end you pay for a single malloc/free (and even this you can reduce if you re-use the buffer multiple times), despite having possibly many more allocations.
The downside is that you have some restrictions, you need to be sure none of the objects in the buffer outlives it, in C++ you probably want to ensure all the objects you allocate from it are trivially destructible, etc.
Either way, it's very common to do this sort of thing, at least in C and C++. That's partially because the system allocator is typically slow, but mostly because this sort of control is a big reason to use languages like this.
I'm not sure this is true in the actual native code produced by the JIT. HotSpot does autobox elimination.
Go is in the same camp as C#/.NET here, where the generational hypothesis certainly holds and therefore .NET uses a generational garbage collector. I am certain that the generational hypothesis holds for Go as well.
Not having a generational GC is a design mistake in Go. Google should fix that.
One of the arguments mentioned against having a generational GC is that it would make concurrent GC with low latency harder (because Go permits interior pointers, unlike Java) and slower (because Go only needs write barrier, unlike Java which needs read barrier as far as I know).
Also, I don't believe that the HotSpot GC needs to use read barriers. Read barriers are only necessary if you're concurrently compacting objects (like Azul C4 does). If you have concurrent mark-and-sweep and stop the world only during compaction phases, read barriers are unnecessary.
I think you're right about HotSpot GC not using read barriers. I was mixing it up with the Azul GC, which uses read barriers.
> If you have concurrent mark-and-sweep and stop the world only during compaction phases, read barriers are unnecessary.
Do you know if it's possible to bound the pause caused by the compaction phase to some ceiling, like 2 ms for example? I'm asking because it's a goal of Go' GC to limit GC pauses.
Are you sure about this (without using C# unsafe context)? I've spent a moment digging in C# documentation and been unable to confirm this.
[1]: https://docs.microsoft.com/en-us/dotnet/csharp/language-refe...
[2]: https://docs.microsoft.com/en-us/dotnet/csharp/language-refe...
I think it matters for this argument.
You are arguing that designing a garbage collector which is concurrent, low latency (pauses < 5 ms), compacting, generational, and supports interior pointers, is easy.
You mentioned C#/.NET as an example, but C# doesn't have interior pointers (from heap to heap, not from stack to heap which is possible).
As far as I know, neither Java, .NET, Go, Haskell, OCaml, D, V8 or Erlang satisfies all these requirements at the same time.
I'm not saying it's impossible, and Ian Lance Taylor in the thread I linked earlier is not saying either. I'm just saying it's certainly hard.
If it would be so easy, then most languages would already have interior pointers and a concurrent, low latency, compacting, generational GC. That's the whole difference between "today" (as in your comment) and "tomorrow".
I don't immediately see why you'd need a read barrier for snapshot-at-the-beginning. You only need to trace references from gray objects to white objects that were deleted, which is a write.
However, ZGC will have read barriers and if Shenandoah ever gets integrated into Hotspot, it has them too.
> Many other garbage-collected languages use explicit safe-points on back-edges, or they use forward-simulation to reach a safe-point. Partly, it's possible for Go to support safe-points everywhere because Go's GC already must have excellent support for interior pointers; in many languages, interior pointers never appear at a safe-point. > [...] > Decoupling stack-move points from GC safe-points. [...] Such optimizations are possible because of Go's non-moving collector.
[1] https://github.com/golang/proposal/blob/master/design/24543-...
Well, firstly the language has value semantics, so in many cases you can read the source code knowing nothing about escape analysis and make inferences about where data lives. With Java, you're at the mercy of escape analysis. Further, Go's escape analysis seems simpler; I've been programming in Java on and off for a decade and I depend on profiling to hunt down allocations (except in very simple cases). This isn't the case in Go despite working with it for only a few years recreationally.
> This goes away eventually
I keep hearing that, but I'm 4 years into Rust and I'm still not able to be reasonably productive. I'm sure it's possible to get there, but I doubt I will ever be as productive in Rust as I am in Go for the class of applications I write.
Are you referring to heap vs. stack? That is not as simple as it seems, because of escape analysis. The only thing that Go lets you do that Java doesn't is put structs inside other structs, which is nice, but it's not a game-changer.
> Further, Go's escape analysis seems simpler; I've been programming in Java on and off for a decade and I depend on profiling to hunt down allocations (except in very simple cases). This isn't the case in Go despite working with it for only a few years recreationally.
I don't see how the HotSpot JVM escape analysis can be "simpler" than that of Go. They work the same way.
This isn't true. I can get a contiguous slice of structs, and I can pass value types by copy without needing to reason about escape analysis at all. If I do `var foo Foo` (assuming Foo is a struct type), I know that `foo` won't escape unless I return a pointer to it (or to something in it), at which point it's subject to escape analysis. I can get quite a long way in Go without needing to reason about escape analysis at all.
> I don't see how the HotSpot JVM escape analysis can be "simpler" than that of Go. They work the same way.
I'm pretty sure they don't work the same way, but I don't have any examples off hand to share (it's been a while since I worked with Java). I think Java's escape analysis is more sophisticated.
1. allocate less, but this is not always straightforward since Go doesn't give you a fine control on memory allocation (compared to Rust). 2. change the VM tier to get more memory, what we eventually did.
The profiling bit is harder than in Rust, but you only need to do this analysis in the hot path, while Rust requires you to make memory allocation decisions everywhere.
I'm going to guess: lack of load testing. Something I imagine you still need to do in Rust too.
However, you can learn and understand all of Erlang's syntax in half a day.
Rust's syntax is... less than optimal.
Coming from a Java and JavaScript background, the Rust syntax was never a problem for me, it instantly felt like home (the only thing that bugged me was the lack or parenthesis around `if` expressions, and the lack of the `return` keyword at the end of a function but that's really no big deal and I internalized it pretty quickly). Some people complain about the angle bracket `<>` but that's from Java !
The type system can be a bit tricky to understand, for people not used to algebraic types, and the borrow checker and memory management story is hard to grasp, but the syntax, I just don't understand the complaints …
If something clicks immediately for you ,might not click immediately (or at all) for others ;) However, Rust's potential syntax issues arise from the fact that they try to express complex concepts within a limited set of characters (and it's a problem any sufficiently complex language will face), so of course people will complain :)
#[get("/<name>/<age>")]
fn hello(name: String, age: u8) -> String {
format!("Hello, {} year old named {}!", age, name)
}Lack of mature libraries is always a problem, syntax is subjective and "time it takes go get anything done" .. which time do you measure? Time-to-first-running-example or Time-to-debugged-productions-ready-code? I have my doubts that Rust takes significantly longer for the second one.
Had the same experience. Rust feels slower to develop, but the type system just saves you so much headache in the long run that you're usually much faster at reaching your goal.
Lack of mature libraries is a normal issue for a language that's 3 years old. It will get better with use.
Like many languages, the syntax becomes a lot more clear after you've used the language a while. Maybe it's me who's been warped, but I've been programming in Rust for long enough that the example you pasted looks perfectly normal. ;)
The motivation to learn a new language and the excitement during the process are huge factors that will push many people to write a web service in Rust.
Why did Rust go with the IMHO ugly closure syntax of
|stuff|
?I think that
stuff ->
is much nicer to look at... :(Also, not presented here, but ‘ is also kind of ugly, was there no short keyword that could have been used? “life or “lt” for “lifetime”? “span”?
So I take it you would like to be able to declare your references as `&life of brian`?
As for lifetimes, that syntax is from OCaml, though they use it for something else. “life” and “lt” seem too short, “span” is overloaded. We tried to explore options but none were clearly better, so we kept what we had.
I hadn't noticed the similarity but it makes sense. This is the OCaml syntax for generics, eg:
type 'a list_with_generic = 'a list;I smiled when reading this post because I absolutely recognize the euphoria of carrying out a world-changing refactor and having it work first time. If this is what the future of backend development feels like, sign me up!
In comparison to rocket, actix-web does not use codegen tricks so is not as ergonomic or as safe - e.g. it cannot statically check that the path variables that you ask for actually exist on the route - but these are things that a simple test suite easily picks up (incidentally actix-web has a nice built-in test server).
Also I was a little concerned that rocket development has stalled somewhat. 26 PRs waiting at time of writing. As I recall the maintainer is teaching a Rust class as Stanford so he's most likely just very busy.
https://actix.github.io/actix-web/actix_web/struct.Route.htm...
I only have minimal experience with it, since I've also eventually chosen actix-web for all my projects so far due to its simplicity.
I’ve been using rust moderately for the last 6 months and at least my experince was that once you get used to playing along with the compiler and understand the fundamental features of the language, it almost feels too good to be true.
Perfect mix of high performance, safety and modern features. Slightly challenging at first, but it really is worth the initial pain.
Also, have you seen the sample code for Rocket based webapps? I haven't seen much nicer in any language. The async ecosystem isn't nearly as nice yet, but it should get much nicer once async-await lands.
Had a quick look just now. Clean syntax for the implementation, but routing and parameters are relying on magic # annotations. Compare with e.g. https://doc.akka.io/docs/akka-http/current/routing-dsl/index... where you can do the routing in plain old code (also async is already there).
Actually, as someone almost finished with the book, that brings up an interesting point. What controls # annotations and how do you hook into them (which also answers the question of how magic they are)?
If it's fairly standard and obvious how to look up, I think I might not classify them as much "magic", but truthfully, I have no idea at all. Maybe I'm just not remembering that part of the book (I did take a break for a month), or maybe I didn't make a connection that was implied?
(I hate akka and actors, but as a user of akka-http you don't have to touch them.)
Ha, I had the same feeling when I needed to create a simple REST service that'd just process a request using command line tools. Seems easy but Rust's tooling is full of rough edges. Code completion sometimes works sometimes doesn't, cargo cannot install dependencies only (without building source) so it's not good for Dockerfile layers, etc. etc. Borrow checker is not bad at all for people that worked with languages with explicit memory management (where you do ownership tracking in your head anyway).
Long story short I spent 2 days working on the service and had 80% done but figured out the rest would take twice as much if I want this to be production quality. I scraped the project and rewritten it in node, using one or two dependencies in 2 hours.
I'll be trying Rust again surely and I'm glad that articles like this one exist!
That said, one of the only remaining things keeping me from using it is production ready gRPC library.
But yep, definitely not production ready as the README clearly states :)
* You are ready to become an active contributor to the project.
* You are brave!
Specifically the custom bindings using Diesel. I was unaware that generic sql could be bound so easily. That’s closer to how I end up when needing to create higher performance queries. I need to take another look at Diesel now.
Very nice article.
This is a very helpful post for me.
Very recently I've taken a tour around the Rust web server ecosystem, and I think it's way too hard to find one to pick.
The author mentions actix-web[0], and mentions how fast it is, but it's actually right below hyper[1], which I find to be simpler, because it doesn't bring in the whole actor frame work thing. Hyper builds on Tokio[2] which introduces the usual event loop paradigm that we're used to in other languages (which AFAIK is the fastest way to write web servers to date). There's also great talk[3] that introduces the choices that tokio makes that are somewhat unique.
Here's what I want out of a web framework:
- express like-interface (func(req,resp,next) pattern provies just the right amount of freedom/structure IMO)
- good routing (plus: decorators/a fairly ergonomic way to specify routes)
- reasonable higher-level interfaces to implement (I want to implement an interface, and be able to receive things like logging, tracing, grpc/thrift/whatever transports for free, good interfaces are what make writing reusable stuff like that possible)
Here's what I found about the various frameworks that exist out there:
- Rocket.rs[4]: Seems to do too much (I tend towards building APIs), rocket is closer to django/rails than it is to flask/sinatra.
- Gotham.rs[5]: Seems perfect, but falls flat on the feature front, half the stuff on the landing page are features of rust, not the library. Doesn't seem to have enough batteries included, for example there's currently work being done on streaming request bodies (https://github.com/gotham-rs/gotham/issues/189), that's not a issue I want to run into.
- Iron.rs[6]: The oldest of the bunch, but also very very fast (which I discovered actually browsing rocket's issues[7]), not based on hyper, and also not actively maintained.
I had no idea which of these to use, or how to find ones I've missed, then I stumbled upon this amazing resource: https://github.com/flosse/rust-web-framework-comparison.
However, when you look at that comparison, it's really suspicious how much of actix-web has filled out, which is often indicative of something written from one point of view (like when people have comparison pages, and one option seems to just have "everything"). And again, like I said actix seems to be doing too much, I don't really want to utilize the actor model, I just want a relatively fast, ergonomic simple library with the most basic batteries included.
BTW, everyone keeps raving about type safety and I wonder why people don't give Haskell more of a go. If Rust gives you the type safety equivalent to a shield, haskell is a phalanx. I've never felt more safe and protected by my types than when I write Haskell -- it's relatively fast, memory safe, has fantastic concurrency primitives, and though it can be tough to optimize (which comes to down to optimizing for lower amounts of GCs like most other memory managed languages), it's pretty fast out of the gate. I use servant (https://haskell-servant.readthedocs.io/) and love it.
[0]: https://github.com/actix/actix-web
[1]: https://hyper.rs/
[2]: https://tokio.rs/
[3]: https://www.youtube.com/watch?v=4QZ0-vIIFug
[4]: https://rocket.rs/
[5]: https://gotham.rs/
With the numbers iron puts up, I would imagine that it's actually doing an event loop somewhere, though, it's way too fast to not be.