How to Build Serverless Applications in Rust

Web
Build serverless Rust apps by creating async handlers with axum and compiling for the wasm32-unknown-unknown target.

How to Build Serverless Applications in Rust

You have a function that processes images or calculates prices. You want it to run fast and only cost money when it runs. You tried Python and the cold starts are killing your latency. You tried Node and the memory usage is spiking. Rust offers a different path. You write the logic once, compile it to a tiny binary or WebAssembly module, and hand it to the cloud. The cloud runs it instantly. No virtual machine overhead. No garbage collection pauses. Just your code, doing the work.

Serverless without the baggage

Serverless means you ship code, not containers. The platform handles the scaling. You pay per invocation. Rust shines here because the compiled artifact is small and starts instantly. The cold start problem, where the first request waits for the environment to boot, disappears when your binary is a few megabytes instead of hundreds.

Many platforms now accept WebAssembly. You compile Rust to WASM, and the platform runs it in a sandbox. It is safer and faster than shipping a full Linux image. The sandbox isolates your code from the host. You get deterministic performance. Memory usage stays flat. The lack of a garbage collector means no random pauses while the runtime cleans up unused objects.

Rust turns the serverless model from a gamble into a guarantee.

Minimal handler

The core of a serverless function is a handler. The handler accepts a request, runs your logic, and returns a response. You define this as an async function. Async allows the runtime to pause your function while waiting for I/O, keeping the thread free for other work.

The axum crate provides routing and extraction. It is a popular choice because it integrates well with the broader Rust ecosystem. You build a router, attach your handlers, and call the router with the incoming request.

use axum::{response::Html, routing::get, Router};
use http::Request;
use tower_service::Service;

/// Handles the root path and returns a simple HTML response.
async fn index() -> Html<&'static str> {
    Html("<h1>Serverless Rust</h1>")
}

/// The entry point for the serverless runtime.
/// It constructs the router and forwards the request.
async fn app(request: Request<String>) -> http::Response<String> {
    // Create the router once per invocation.
    // In optimized setups, you might cache this, but for simplicity:
    let router = Router::new().route("/", get(index));

    // Call the router as a service and unwrap the result.
    // Serverless runtimes expect a concrete Response type.
    router.call(request).await.unwrap()
}

Compile the project for WebAssembly. The target wasm32-unknown-unknown produces a WASM module. The --release flag is mandatory. Debug builds are slow and large. The release build optimizes the code for speed and size.

cargo build --target wasm32-unknown-unknown --release

Compile to WASM and ship the logic, not the baggage.

What happens at runtime

When the cloud platform receives a request, it loads your WASM module. It calls your handler. The handler runs your async logic. The response goes back. If the platform keeps the instance warm, the next request skips the load.

The axum router acts as a tower::Service. This trait defines how to process a request. The call method takes the request and returns a future. The future resolves to the response. The runtime awaits this future.

Some runtimes call your function synchronously. You cannot return a future directly. You need to block on the future to get the result. The futures_executor crate provides a simple executor for this. It runs the async code on the current thread and blocks until completion. This bridges the gap between async Rust and sync runtimes.

Rust's ownership model keeps the request and response isolated. You cannot accidentally share mutable state between requests. The compiler enforces this. If you try to move a value out of a borrowed request, the compiler rejects you with E0507 (cannot move out of borrowed content). This prevents data races and memory corruption.

Trust the borrow checker. It usually has a point.

Realistic example with JSON and configuration

Real applications return JSON and read configuration. You need serde for serialization and serde_json for JSON parsing. You also need to configure the crate type in Cargo.toml. The cdylib crate type tells Rust to produce a dynamic library, which is the standard format for WASM modules.

[lib]
crate-type = ["cdylib"]

[dependencies]
axum = { version = "0.7", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

The handler extracts query parameters and returns a JSON response. Error handling is explicit. You match the result of the router call. If the route matches, you return the response. If it fails, you return a 500 error.

use axum::{
    extract::Query,
    response::Json,
    routing::get,
    Router,
};
use http::Request;
use serde::Deserialize;
use tower_service::Service;

/// Extract query parameters from the URL.
#[derive(Deserialize)]
struct Params {
    name: String,
}

/// Returns a JSON greeting based on the query parameter.
async fn greet(Query(params): Query<Params>) -> Json<serde_json::Value> {
    Json(serde_json::json!({
        "message": format!("Hello, {}!", params.name)
    }))
}

/// Serverless handler with routing and error handling.
async fn app(request: Request<String>) -> http::Response<String> {
    let router = Router::new().route("/greet", get(greet));

    // Match the route and return the response.
    // If the route doesn't match, axum returns a 404.
    match router.call(request).await {
        Ok(response) => response,
        Err(e) => http::Response::builder()
            .status(500)
            .body(format!("Internal error: {}", e))
            .unwrap(),
    }
}

If you forget to derive Deserialize for your query struct, the compiler rejects you with E0277 (trait bound not satisfied). The Query extractor needs that trait to parse the URL. The error message points to the struct and tells you to add the derive macro.

Convention aside: The community prefers serde_json::json! for quick literals and serde::Serialize for structured responses. It keeps the code readable and type-safe.

Handle errors explicitly. The cloud won't forgive a panic.

Pitfalls and constraints

WASM targets lack the full standard library. You cannot read files or open sockets directly. You must use the platform's provided APIs. If you try to use std::fs, the build fails. The linker cannot find the symbols. You need to check the platform documentation for the available bindings.

Memory is capped. A leak in Rust won't crash the system, but it will crash your worker instance. The platform kills it. You need to manage resources. Avoid unbounded caches. Use LruCache or similar structures with limits.

Rust developers love lazy_static for singletons. In serverless, a singleton persists across requests if the instance stays warm. This can be a feature or a bug. If you cache a user token, you might leak it to the next user. You must isolate state per request or use thread-local storage. The compiler won't stop you, but the architecture will bite you.

Connection pooling is tricky. Serverless functions are ephemeral. You cannot keep a connection pool alive easily. You need a proxy or a library that handles this. Opening a new database connection per request is slow. You need a strategy that balances latency and cost.

Respect the sandbox. The platform sets the rules, and Rust enforces them.

Decision matrix

Use Rust for serverless when cold start latency is the bottleneck and you need sub-millisecond initialization. Use Rust when your function performs heavy computation, like image processing or cryptography, and you want to minimize compute costs per request. Use Rust when you are building a library that needs to be shared between the serverless function and a browser client. Reach for Python or Node.js when you rely on a massive ecosystem of third-party packages that don't compile to WebAssembly. Reach for a native binary when the serverless platform supports it and you need direct access to system calls or C libraries.

Pick the tool that matches the constraint. Speed needs Rust. Speed of development might need something else.

Where to go next