Once a Rust service needs to handle many connections at once, you reach for async Rust and the Tokio runtime — the same combination behind most production Rust network code. This guide explains the shape of an async Rust service on Falix, gives you a minimal Tokio server that was verified to build and run on the real runtime, and is honest about the one cost that matters here: compile time.
| At a glance | |
|---|---|
| You need | A Falix server running the Rust application (some RAM headroom helps — Rust is memory-hungry to compile) |
| Plan | Any — on free it runs while your session timer has time left, premium runs 24/7 |
| Time | Twenty-five minutes, most of it the first build |
| New to the Rust app? | Rust on Falix |
What async buys you
A plain, synchronous server dedicates a whole OS thread to each connection while it waits on I/O. Async Rust flips that: async fn bodies become tasks that yield whenever they'd block, so one small pool of threads can juggle thousands of connections. You don't write threads — you write async functions and .await the slow parts, and Tokio schedules the rest.
Tokio is the runtime that makes this real: an executor, an async-aware TCP/UDP stack, timers, and channels. You opt in with the #[tokio::main] macro, which turns a normal main into an async one running on Tokio.
Adding Tokio
Add it in Cargo.toml:
[dependencies]
tokio = { version = "1", features = ["full"] }
Or open the Packages page in your server menu, search tokio, and press Install — it runs cargo add and updates Cargo.toml for you (see Rust on Falix). The full feature set is the easy starting point; you can trim it to just what you use later to shave build time.
A minimal async server
Here's a complete src/main.rs: an echo server that reads SERVER_PORT, binds all interfaces, accepts connections, and echoes back whatever it receives — spawning a lightweight task per client so many can be served at once.
use std::env;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let port = env::var("SERVER_PORT").unwrap_or_else(|_| "8080".to_string());
let addr = format!("0.0.0.0:{}", port);
let listener = TcpListener::bind(&addr).await?;
println!("Listening on {}", addr);
loop {
let (mut socket, _peer) = listener.accept().await?;
tokio::spawn(async move {
let mut buf = vec![0u8; 1024];
loop {
match socket.read(&mut buf).await {
Ok(0) => return, // connection closed
Ok(n) => {
if socket.write_all(&buf[..n]).await.is_err() {
return;
}
}
Err(_) => return,
}
}
});
}
}
Three things to notice, because they generalise to any Tokio service:
#[tokio::main]starts the runtime and runs your asyncmainon it..awaitonbind,accept,read, andwrite_allis where the task politely steps aside so other tasks can run.tokio::spawnhands each connection to its own task.acceptimmediately loops back for the next client, so a slow connection never blocks new ones.
🎯 Good to know: Binding
0.0.0.0onSERVER_PORTis the universal Falix web rule — it's what makes the service reachable from outside. Never bindlocalhost/127.0.0.1. See Your first web app.
Building an HTTP API rather than a raw TCP server? Reach for an async web framework built on Tokio (axum and hyper are the common picks). The runtime shape is the same — #[tokio::main], read SERVER_PORT, bind 0.0.0.0 — you just define routes instead of reading bytes. The Tokio documentation is the place to go for the full async story, channels, and the wider ecosystem.
The honest part: compile time
The Rust application runs cargo run --release on every start, compiling from source (see Rust on Falix). The moment you add Tokio, that first build gets bigger: Tokio pulls in a real dependency tree — mio, socket2, parking_lot, bytes, and friends — and all of it compiles before your code does.
- The first start is slow. Cargo compiles the whole tree once. Watch the console for
Compiling …lines, thenFinished, thenListening on …. It isn't stuck; async brings dependencies, and dependencies take time. - Later starts are fast. Cargo caches everything in
target/, which survives restarts, so you pay the long build once — not every start. - A reinstall resets the cache. Switching applications or reinstalling wipes
target/, so the next start pays the full first-build cost again. - Watch memory on small plans. Compiling a big async dependency tree is memory-hungry; on the free plan's shared RAM a heavy build can be killed (exit code 137). Keep your dependency list lean, and see Out of memory.
💡 Tip: Trimming Tokio's
featuresfrom"full"to only what you use (for a TCP server,["rt-multi-thread", "macros", "net", "io-util"]) compiles less and starts faster — worth doing once your service settles.