Rust for Cloud Services: When and How

February 22, 2021

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 isn’t the right choice for every service. But for performance-critical workloads, it offers a compelling combination of speed and safety.