Rust development builds became unbearably slow. Then I found a way to get ~1 second rebuilds!

I really like Rust, and I'm honestly in love with it. I never thought I'd go back to Go, especially considering some of its significant shortcomings for me: undefined nil, lack of proper enums and match, no Result or Option, etc.

But over the last few weeks, as my projects have grown significantly, I've started seeing development build times (edit → save → run) of 30–50 seconds, even after trying every remedy (for example, everything listed here).

I can't wait that long.

I need to use Windows and my Rust applications are almost all web servers, and after even small code changes, I need to be able to immediately verify that everything still works — manually, without automated tests or e2e.

So the time between pressing Ctrl + S and having my Axum server reload (using watchexec) has become unbearable.

I was seriously considering dropping everything and going back to Go.

Then I discovered this: GitHub - dennj/axum-hotreload: rust axum hotreload · GitHub.

I'm not the author of that project!

It solves most of my problems because the exact thing I can't stand anymore — waiting for the project to rebuild — is what this fantastic project addresses.

There are still a few bugs. I've filed an issue, but I think I've also fixed one of them in a local build, so I'll update the issue soon.

Do you think there could be problems with using this in daily development? I haven't migrated my real projects yet because I'm currently restructuring everything and removing the incredibly slow async-graphql.

I tried a project with 1,000 Axum routes, and from the moment I save the file to the moment the server is ready again, it takes about 1 second.

I couldn't believe it!

What do you think? Is there anything better? Are there any problems with using such solutions in development?

But this hotreload invokes cargo build -p your-project --lib under the hood, so it shouldn't be faster... probably something in your IDE such as a misconfigured rust-analyzer?

TBH, this sounds like a classic problem of being overly-static-dispatched. Is it possible that incremental is so slow because there's one giant Future coupled to everything?

I would expect it to be entirely feasible to have the routing layer make one dyn call to the handler, and thus not have to recompile more than a single handler when you change a handler, which should have great incremental performance.

(Said assuming you've checked that you're not just doing a full build every time from bad proc macros, inconsistent configs, or whatever.)

Also, if you find that you often update strings but not code, you can use...

(from Is it reliable to use cargo miri run instead of cargo run during development? Because it compiles much faster - #23 by kpreid)

I think it's a very neat solution. Why couldn't I discover it? Indeed, instead of every time recompile entire Axum server, it's reasonable to define it on a lever of API only and recompile an app only, and then inject it in the real server. Bravo. Anyway, I stick with another solution which doesn't require to recompile server too. For example I debug my app now on the server which I started in March.

It is calling cargo build of the single project i define as cdylib. So it is fast as hell!

Maybe, i'm using async-graphql with only 3 axum routes and 1 for it. I'm currently removing async-graphql.

Can you better explain this, please?

Thanks but is very difficult to use this in an axum project with many routes, at least for me!

How?

I have two types of apps:

  • CGI script
  • CGI Websocket andpoint

First just starts an app for servicing each request. So, as only I rebuild my app, the new version will be used. Second type is a bit more complex. Same copy of an app remains in memory and continues processing packets. I have to reload a page to make sure that an old app gone. I can't recompile it on Windows until all usages of the app stopped, so I prefer Linux or Mac OS for debugging. I do not use Mac OS now though.

If you have a lot HTTP requests, my approach won't work well for you. However if most processing happens on websocket level, my approach is perfect.

I have no idea if this is anything to do with your problem but I recently had a problem with builds of running to nearly five minutes after making changes that I thought should not trigger a full rebuild. I'm using Axum and SurrealDb

Turned out that I had surrealdb specified as a dependency in the top level Cargo.toml and in the Cargo.toml of a workspace member. The problem was that there were different features enabled in each place. Which somehow triggered a rebuild by cargo all the time.

The fix was to change the surreal dependency in Cargo.toml in the workspace member to:

surrealdb = { workspace = true }

No full rebuilds since then.