
Fast, Safe, and Complete(ish) Web Service in Rust - henridf
https://brandur.org/rust-web
======
masklinn
Really nice introductory article, though some of the more… axiomatic
assertions, are iffy.

> 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](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.

~~~
nordsieck
> Synchronous sequences are generally faster than asynchronous ones because
> yielding and getting scheduled back has a non-zero overhead

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.

[0] [https://www.slideshare.net/e456/tyma-
paulmultithreaded1](https://www.slideshare.net/e456/tyma-paulmultithreaded1)

[1] [http://www.kegel.com/c10k.html](http://www.kegel.com/c10k.html)

~~~
fafhrd91
i'd say in most projects i worked, threads would be enough. but it is so
boring :) new shiny async code is much more entertaining!

on other hand we should talk about C100k or C1m problems.

~~~
nordsieck
Someone actually coined the C10M problem a while back [0]. It looks like it
hasn't been updated in quite a while, though.

[http://c10m.robertgraham.com/p/blog-
page.html](http://c10m.robertgraham.com/p/blog-page.html)

------
Thaxll
Why would you want to do that in Rust instead of Go / Java / C# since the
performance is the same. When I look at code example in the article the syntax
is just awful seriously, who want to write web services with that kind of
syntax:

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.

~~~
staticassertion
Nonsense to you is wonderful to me. I don't think "I am unfamiliar with this
syntax" is really a useful observation - Erlang has an extremely unfamiliar
syntax, but I don't think that makes it less suitable for building a service.

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.

~~~
weberc2
> 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.

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).

~~~
pcwalton
How exactly is the Go GC easier to "triage" than that of the JVM?

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.

~~~
ngrilly
> 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.

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.

~~~
littlestymaar
> This puts a lot of pressure on the GC because a lot of multiple small
> objects are created.

… 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.

~~~
ngrilly
I already agreed that allocation is faster in Java than in Go. What I'm saying
is that allocation speed is less critical in Go than in Java.

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.

~~~
littlestymaar
> What I'm saying is that allocation speed is less critical in Go than in
> Java.

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.

~~~
ngrilly
> Go couldn't afford not to have a generational GC without value type

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?

~~~
littlestymaar
> 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).

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...](https://groups.google.com/forum/#!topic/golang-
nuts/Vcgx7hkhd3Q)

~~~
ngrilly
It's not "unfortunate". It's a known drawback of the tradeoff chosen by Go.

~~~
pcwalton
Which can be effectively mitigated by having a generational garbage collector!

------
adwhit
I've been using 'actix-web' in anger for the last month or so and I just want
to echo that it is an amazingly fast and fully-featured project.

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!

~~~
dorfsmay
Have you look at rocket etc... Curious about your decision process.

~~~
adwhit
I like rocket a lot - practically as easy as writing flask but with glorious
type-safety thrown in - but I really wanted async ('like, now') because my
project will have a large number of persistent connections. I'm sure rocket
will get async eventually but it looks to be some way off.

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.

~~~
fafhrd91
i just added extractors system. it is not released yet, i am planing to
release it later this week

[https://actix.github.io/actix-
web/actix_web/struct.Route.htm...](https://actix.github.io/actix-
web/actix_web/struct.Route.html#method.with)

------
illuminati1911
Really nice article.

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.

------
lmm
The author briefly mentions Haskell but I wonder whether they've build web
services with it, or with another (loosely) ML-family language (e.g. OCaml,
F#, or my personal favourite Scala). An ML-style type system is a huge
advantage over Ruby/JavaScript/..., but for a web service I struggle to see a
real case for going to all the effort of memory ownership tracking rather than
using a GCed language with the same kind of strong type system; it would be
good to see a comparison from someone who's done both.

~~~
nicoburns
I think that despite the constraints imposed by the lack of GC, Rust is seen
as more approachable than more functional langauges to those coming from a
C/Java/JavaScript etc background. And the only similar language with an ML
type system is Swift which isn't quite there with libraries for backend stuff.

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.

~~~
lmm
> Also, have you seen the sample code for Rocket based webapps? I haven't seen
> much nicer in any language.

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...](https://doc.akka.io/docs/akka-http/current/routing-
dsl/index.html#longer-example) where you can do the routing in plain old code
(also async is already there).

~~~
kbenson
> routing and parameters are relying on magic # annotations

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?

~~~
jrhurst
Look into macros 1.1 to see how they're use for `#derived` they're not
particularly super magical. Though unstable stuff rocket uses is a bit more
magical.

------
Shoothe
> It’d be fair to say that I could’ve written an equivalent service in Ruby in
> a tenth of the time it took me to write this one in Rust.

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!

------
vardump
Very exciting to see Rust gaining support lately.

That said, one of the only remaining things keeping me from using it is
production ready gRPC library.

~~~
kehtnok
Just an FYI (if you want something to watch and haven't heard about it
already) [https://github.com/tower-rs/tower-grpc](https://github.com/tower-
rs/tower-grpc) is in the works by the same folks building tokio and hyper etc.

But yep, definitely not production ready as the README clearly states :)

~~~
carllerche
I would say that you __could __start using it if:

* You are ready to become an active contributor to the project.

* You are brave!

------
bluejekyll
Thank you for writing/posting this!

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.

------
staticassertion
Thanks for writing this. Actix is really cool but I haven't really spent the
time looking into it - I was unaware of the SyncArbiter abstraction.

This is a very helpful post for me.

------
ComputerGuru
I _love_ rust, but to be honest, until the nightmare that is tokio/futures is
fixed with native async/await and better compiler error messages, a strongly
typed language like C# with those features natively present is my choice for
web services. It addresses all the author’s issues with Ruby and JS, and is
still orders of magnitude faster than those options (though admittedly not as
fast as a C or rust option).

------
rabidferret
Just wanted to mention that your usage of `sql_query` is exactly how it's
meant to be used, and exactly where I would reach for it. Great article!

------
hardwaresofton
tldr; - it was hard for me to determine which rust web framework I should be
using, since I want something light like flask. best resource was
[https://github.com/flosse/rust-web-framework-
comparison](https://github.com/flosse/rust-web-framework-comparison)

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](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](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/](https://haskell-
servant.readthedocs.io/)) and love it.

[0]: [https://github.com/actix/actix-web](https://github.com/actix/actix-web)

[1]: [https://hyper.rs/](https://hyper.rs/)

[2]: [https://tokio.rs/](https://tokio.rs/)

[3]:
[https://www.youtube.com/watch?v=4QZ0-vIIFug](https://www.youtube.com/watch?v=4QZ0-vIIFug)

[4]: [https://rocket.rs/](https://rocket.rs/)

[5]: [https://gotham.rs/](https://gotham.rs/)

[6]: [http://ironframework.io/](http://ironframework.io/)

[7]:
[https://github.com/SergioBenitez/Rocket/issues/552](https://github.com/SergioBenitez/Rocket/issues/552)

~~~
huntie
Iron does use hyper, but it uses an older, synchronous version of hyper. Iron
also isn't actively maintained.

~~~
hardwaresofton
Thanks for the clarification -- I was wondering what iron was using under the
covers but didn't care enough to down and look.

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.

