Only show guides for your app
How async/await and the Tokio runtime shape a Rust service — with a verified minimal echo server that reads SERVER_PORT — plus the honest truth about compile time on this runtime.
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 |
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.
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.
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 async main on it..await on bind, accept, read, and write_all is where the task politely steps aside so other tasks can run.tokio::spawn hands each connection to its own task. accept immediately 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 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.
Compiling … lines, then Finished, then Listening on …. It isn't stuck; async brings dependencies, and dependencies take time.target/, which survives restarts, so you pay the long build once — not every start.target/, so the next start pays the full first-build cost again.💡 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.