Rust has moved beyond systems programming into cloud services. Companies like Discord, Cloudflare, and AWS use Rust for performance-critical services. The promise of C-level performance with memory safety is compelling.
Here’s when Rust makes sense for cloud services and how to start.
Why Consider Rust
The Value Proposition
Traditional tradeoffs:
Go: Fast enough, productive, GC pauses
Java: Productive, mature ecosystem, memory overhead
Python: Very productive, slow
C/C++: Fast, dangerous, less productive
Rust offers:
- Performance comparable to C/C++
- Memory safety without garbage collection
- No GC pauses
- Strong type system
- Modern tooling
Real-World Results
discord:
problem: Go GC pauses causing latency spikes
solution: Rewrote Read States service in Rust
result: Consistent latency, no GC pauses
cloudflare:
problem: Performance-critical edge processing
solution: Rust for Workers runtime, HTTP/3, etc.
result: Low latency, high throughput
aws:
problem: Firecracker needs performance + safety
solution: Rust microVM
result: Sub-second VM startup, secure isolation
When Rust Makes Sense
Good Fit
performance_critical:
- Latency-sensitive services
- High-throughput processing
- Resource-constrained environments
- Real-time requirements
memory_sensitive:
- Limited memory budget
- Predictable memory usage needed
- No GC pause tolerance
- Edge/embedded deployment
safety_critical:
- Security-sensitive code
- Memory safety requirements
- Concurrent code
- Long-running services
Less Ideal
consider_alternatives_when:
- Rapid prototyping needed
- Team unfamiliar with Rust
- Simple CRUD application
- Time-to-market critical
- Ecosystem gaps for your domain
productivity_tradeoff:
- Steeper learning curve
- Longer compile times
- More upfront design
- Borrow checker learning
Getting Started
Project Setup
# Create new project
cargo new my-service
cd my-service
# Add dependencies (Cargo.toml)
[package]
name = "my-service"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1", features = ["full"] }
axum = "0.6"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tracing = "0.1"
tracing-subscriber = "0.3"
[profile.release]
lto = true
codegen-units = 1
Basic HTTP Service
use axum::{
routing::{get, post},
Json, Router,
};
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
#[derive(Serialize, Deserialize)]
struct User {
id: u64,
name: String,
}
async fn get_user() -> Json<User> {
Json(User {
id: 1,
name: "Alice".to_string(),
})
}
#[derive(Deserialize)]
struct CreateUser {
name: String,
}
async fn create_user(Json(payload): Json<CreateUser>) -> Json<User> {
Json(User {
id: 2,
name: payload.name,
})
}
#[tokio::main]
async fn main() {
tracing_subscriber::init();
let app = Router::new()
.route("/users", get(get_user))
.route("/users", post(create_user));
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
tracing::info!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
Database Integration
use sqlx::postgres::{PgPool, PgPoolOptions};
async fn setup_database() -> PgPool {
PgPoolOptions::new()
.max_connections(5)
.connect("postgres://user:pass@localhost/mydb")
.await
.expect("Failed to create pool")
}
async fn get_users(pool: &PgPool) -> Result<Vec<User>, sqlx::Error> {
sqlx::query_as!(
User,
"SELECT id, name FROM users"
)
.fetch_all(pool)
.await
}
// With Axum state
use axum::extract::State;
async fn list_users(State(pool): State<PgPool>) -> Json<Vec<User>> {
let users = get_users(&pool).await.unwrap();
Json(users)
}
#[tokio::main]
async fn main() {
let pool = setup_database().await;
let app = Router::new()
.route("/users", get(list_users))
.with_state(pool);
// ...
}
Error Handling
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("not found")]
NotFound,
#[error("internal error")]
Internal(#[from] anyhow::Error),
#[error("database error")]
Database(#[from] sqlx::Error),
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let status = match &self {
AppError::NotFound => StatusCode::NOT_FOUND,
AppError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
AppError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, self.to_string()).into_response()
}
}
// Handler with error handling
async fn get_user_by_id(
State(pool): State<PgPool>,
Path(id): Path<i64>,
) -> Result<Json<User>, AppError> {
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
.fetch_optional(&pool)
.await?
.ok_or(AppError::NotFound)?;
Ok(Json(user))
}
Async Runtime
Tokio Basics
use tokio::time::{sleep, Duration};
use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
// Spawn concurrent tasks
let handle1 = tokio::spawn(async {
sleep(Duration::from_secs(1)).await;
"task 1 done"
});
let handle2 = tokio::spawn(async {
sleep(Duration::from_secs(2)).await;
"task 2 done"
});
// Wait for both
let (r1, r2) = tokio::join!(handle1, handle2);
println!("{:?}, {:?}", r1, r2);
// Channels for communication
let (tx, mut rx) = mpsc::channel(32);
tokio::spawn(async move {
tx.send("message").await.unwrap();
});
while let Some(msg) = rx.recv().await {
println!("received: {}", msg);
}
}
Concurrent Processing
use futures::stream::{self, StreamExt};
async fn process_items(items: Vec<Item>) -> Vec<Result<Output, Error>> {
stream::iter(items)
.map(|item| async move {
process_item(item).await
})
.buffer_unordered(10) // 10 concurrent
.collect()
.await
}
Testing
Unit Tests
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_create_user() {
let user = User {
id: 1,
name: "Alice".to_string(),
};
assert_eq!(user.name, "Alice");
}
#[tokio::test]
async fn test_handler() {
let app = Router::new().route("/users", get(get_user));
let response = app
.oneshot(Request::builder().uri("/users").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
}
Integration Tests
// tests/integration_test.rs
use sqlx::PgPool;
#[sqlx::test]
async fn test_database_operations(pool: PgPool) {
// Test with real database
let user = create_user(&pool, "Test").await.unwrap();
assert_eq!(user.name, "Test");
}
Deployment
Docker Build
# Multi-stage build for small images
FROM rust:1.70 as builder
WORKDIR /app
COPY . .
RUN cargo build --release
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/my-service /usr/local/bin/
EXPOSE 3000
CMD ["my-service"]
Optimized Build
# Cargo.toml
[profile.release]
lto = true # Link-time optimization
codegen-units = 1 # Better optimization
panic = "abort" # Smaller binary
strip = true # Strip symbols
Common Patterns
Configuration
use config::{Config, Environment, File};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct Settings {
pub database_url: String,
pub server_port: u16,
pub log_level: String,
}
impl Settings {
pub fn new() -> Result<Self, config::ConfigError> {
Config::builder()
.add_source(File::with_name("config/default"))
.add_source(Environment::with_prefix("APP"))
.build()?
.try_deserialize()
}
}
Graceful Shutdown
use tokio::signal;
async fn shutdown_signal() {
let ctrl_c = async {
signal::ctrl_c().await.expect("failed to listen for ctrl+c");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("failed to install signal handler")
.recv()
.await;
};
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
tracing::info!("shutdown signal received");
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(|| async { "Hello" }));
axum::Server::bind(&addr)
.serve(app.into_make_service())
.with_graceful_shutdown(shutdown_signal())
.await
.unwrap();
}
Key Takeaways
- Rust offers C-level performance with memory safety; no GC pauses
- Best for performance-critical, latency-sensitive, or resource-constrained services
- Consider learning curve and development speed tradeoffs
- Axum provides ergonomic HTTP server framework with Tokio async runtime
- SQLx offers compile-time checked database queries
- Strong error handling with Result types and thiserror
- Multi-stage Docker builds produce small, efficient images
- Tokio ecosystem provides async runtime and utilities
- Start with a non-critical service to learn; migrate critical services later
Rust isn’t the right choice for every service. But for performance-critical workloads, it offers a compelling combination of speed and safety.