Framework Guide
Rust API Monitoring
Set up health check endpoints and uptime monitoring for your Rust API. Covers Actix-web, Axum, serde responses, and database health checks with sqlx and diesel.
Why Monitor Your Rust API?
Rust eliminates entire classes of bugs at compile time, but production failures still happen. Infrastructure goes down, TLS certificates expire, database connections get exhausted, and deployments introduce regressions. External monitoring catches what the compiler cannot.
- Infrastructure failures -- Your Rust binary is rock-solid, but the VM, load balancer, or DNS can still fail
- Connection pool exhaustion -- Database pools (sqlx, diesel, deadpool) can run dry under load
- Dependency outages -- External APIs, caches, and message queues go down independently
- Configuration errors -- Wrong environment variables, expired secrets, or misconfigured TLS after a deploy
Health Response with Serde
Define a typed health response that serializes consistently. Use an enum for status values so invalid states are unrepresentable.
use serde::Serialize;
#[derive(Serialize)]
#[serde(rename_all = "lowercase")]
enum HealthStatus {
Healthy,
Degraded,
Unhealthy,
}
#[derive(Serialize)]
struct HealthResponse {
status: HealthStatus,
version: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
checks: Option<DependencyChecks>,
}
#[derive(Serialize)]
struct DependencyChecks {
database: CheckResult,
}
#[derive(Serialize)]
struct CheckResult {
status: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
response_time_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<String>,
}
Actix-web Health Endpoint
use actix_web::{web, App, HttpServer, HttpResponse};
async fn health() -> HttpResponse {
HttpResponse::Ok().json(HealthResponse {
status: HealthStatus::Healthy,
version: env!("CARGO_PKG_VERSION"),
checks: None,
})
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/health", web::get().to(health))
// ... your other routes
})
.bind("0.0.0.0:8080")?
.run()
.await
}
Axum Health Endpoint
use axum::{routing::get, Json, Router};
async fn health() -> Json<HealthResponse> {
Json(HealthResponse {
status: HealthStatus::Healthy,
version: env!("CARGO_PKG_VERSION"),
checks: None,
})
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/health", get(health));
// ... your other routes
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}
Shared Application State
Health checks need access to your database pool and other shared resources. Both frameworks provide extractors for this.
Actix-web with web::Data:
use actix_web::web;
use sqlx::PgPool;
struct AppState {
db: PgPool,
}
async fn health(state: web::Data<AppState>) -> HttpResponse {
let db_check = check_database(&state.db).await;
let status = if db_check.status == "ok" {
HealthStatus::Healthy
} else {
HealthStatus::Unhealthy
};
let code = match status {
HealthStatus::Healthy => 200,
_ => 503,
};
HttpResponse::build(
actix_web::http::StatusCode::from_u16(code).unwrap()
).json(HealthResponse {
status,
version: env!("CARGO_PKG_VERSION"),
checks: Some(DependencyChecks { database: db_check }),
})
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let pool = PgPool::connect("postgres://localhost/mydb")
.await
.expect("Failed to connect to database");
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(AppState { db: pool.clone() }))
.route("/health", web::get().to(health))
})
.bind("0.0.0.0:8080")?
.run()
.await
}
Axum with State extractor:
use axum::extract::State;
use std::sync::Arc;
struct AppState {
db: PgPool,
}
async fn health(
State(state): State<Arc<AppState>>,
) -> (axum::http::StatusCode, Json<HealthResponse>) {
let db_check = check_database(&state.db).await;
let status = if db_check.status == "ok" {
HealthStatus::Healthy
} else {
HealthStatus::Unhealthy
};
let code = match status {
HealthStatus::Healthy => axum::http::StatusCode::OK,
_ => axum::http::StatusCode::SERVICE_UNAVAILABLE,
};
(code, Json(HealthResponse {
status,
version: env!("CARGO_PKG_VERSION"),
checks: Some(DependencyChecks { database: db_check }),
}))
}
#[tokio::main]
async fn main() {
let pool = PgPool::connect("postgres://localhost/mydb")
.await
.expect("Failed to connect to database");
let state = Arc::new(AppState { db: pool });
let app = Router::new()
.route("/health", get(health))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}
Database Health Checks
Check your database connection with a timeout to prevent a slow database from hanging your health endpoint.
With sqlx:
use sqlx::PgPool;
use std::time::{Duration, Instant};
use tokio::time::timeout;
async fn check_database(pool: &PgPool) -> CheckResult {
let start = Instant::now();
match timeout(
Duration::from_secs(5),
sqlx::query("SELECT 1").execute(pool),
).await {
Ok(Ok(_)) => CheckResult {
status: "ok",
response_time_ms: Some(start.elapsed().as_millis() as u64),
error: None,
},
Ok(Err(e)) => CheckResult {
status: "error",
response_time_ms: Some(start.elapsed().as_millis() as u64),
error: Some(e.to_string()),
},
Err(_) => CheckResult {
status: "error",
response_time_ms: Some(5000),
error: Some("database check timed out".to_string()),
},
}
}
With diesel:
use diesel::prelude::*;
use diesel::r2d2::{ConnectionManager, Pool};
use std::time::Instant;
type DbPool = Pool<ConnectionManager<PgConnection>>;
fn check_database_diesel(pool: &DbPool) -> CheckResult {
let start = Instant::now();
match pool.get() {
Ok(mut conn) => {
match diesel::sql_query("SELECT 1").execute(&mut conn) {
Ok(_) => CheckResult {
status: "ok",
response_time_ms: Some(start.elapsed().as_millis() as u64),
error: None,
},
Err(e) => CheckResult {
status: "error",
response_time_ms: Some(start.elapsed().as_millis() as u64),
error: Some(e.to_string()),
},
}
},
Err(e) => CheckResult {
status: "error",
response_time_ms: Some(start.elapsed().as_millis() as u64),
error: Some(format!("pool error: {}", e)),
},
}
}
Set Up Monitoring with UptimeSignal
Once your health endpoint is deployed, add it to UptimeSignal for continuous external monitoring. UptimeSignal pings your endpoint on a schedule and alerts you immediately when it returns a non-2xx status or times out.
- Sign up at app.uptimesignal.io -- free, no credit card
- Add a monitor with your health endpoint URL (e.g.
https://api.example.com/health) - Set the interval -- 5 minutes on free, 1 minute on Pro
- Configure alerts -- get notified by email when your API goes down or recovers
Best Practices
- Keep it fast -- Health checks should respond in under 500ms. Use timeouts on all dependency checks.
- No auth required -- Monitoring services need unauthenticated access to your health endpoint.
- Use typed responses -- Derive
Serializeon structs instead of building JSON by hand. The compiler catches schema drift. - Separate liveness from readiness --
/healthfor "process is alive",/readyfor "dependencies are connected". - Include version info -- Use
env!("CARGO_PKG_VERSION")to embed the crate version at compile time.