RUST Contents

Request IDs and Correlation

Propagate or generate a request ID, return it to clients, and attach it to logs so incident debugging becomes a lookup instead of a search. Use correlation IDs heavily in logs and traces, but keep metrics cardinality under control.

On this page

Why request IDs change everything in production

When a user reports a failure, the fastest path to resolution is a single identifier you can paste into log search and trace views. Without a request id, you end up guessing based on timestamps, paths, and partial context. With a request id, support and on-call become dramatically more effective: find the request, see the timeline, and identify the root cause.

Production goals at this level

  • Propagate: if the client already sent an id, keep it (do not create a new one).
  • Generate: if missing, generate a new id once at the edge.
  • Echo: always return it to clients so bug reports become actionable.
  • Attach: include it in logs (and later traces) for reliable correlation.

Header choice

A common convention is the x-request-id header. In multi-service systems you might also use trace headers, but for this stage, x-request-id is a simple and effective baseline.

Middleware: read or generate request id

This middleware does three things:

  • Reads x-request-id from the incoming request if present.
  • Generates a UUID if missing or invalid.
  • Inserts the id into both request headers and response headers.
use axum::{
    http::{HeaderMap, HeaderName, HeaderValue, Request},
    middleware::Next,
    response::Response,
};
use uuid::Uuid;

static REQ_ID: HeaderName = HeaderName::from_static("x-request-id");

fn is_reasonable_id(s: &str) -> bool {
    // Minimal validation: non-empty and not absurdly long.
    // Keep this simple at first. You can tighten later if needed.
    !s.is_empty() && s.len() <= 128
}

pub async fn request_id_middleware<B>(mut req: Request<B>, next: Next<B>) -> Response {
    let incoming = req
        .headers()
        .get(&REQ_ID)
        .and_then(|v| v.to_str().ok())
        .map(|s| s.to_string());

    let rid = match incoming {
        Some(v) if is_reasonable_id(&v) => v,
        _ => Uuid::new_v4().to_string(),
    };

    // Make it available to downstream handlers and layers.
    req.headers_mut()
        .insert(&REQ_ID, HeaderValue::from_str(&rid).unwrap());

    // Continue the pipeline.
    let mut res = next.run(req).await;

    // Echo back to clients for support and debugging.
    res.headers_mut()
        .insert(&REQ_ID, HeaderValue::from_str(&rid).unwrap());

    res
}

Attach request id to logs

Correlation only works if your logs include the id. The simplest form is logging once per request at the middleware edge. Later you can attach it to spans so every downstream log line inherits it.

pub async fn request_id_middleware<B>(mut req: Request<B>, next: Next<B>) -> Response {
    let incoming = req
        .headers()
        .get(&REQ_ID)
        .and_then(|v| v.to_str().ok())
        .map(|s| s.to_string());

    let rid = match incoming {
        Some(v) if is_reasonable_id(&v) => v,
        _ => uuid::Uuid::new_v4().to_string(),
    };

    req.headers_mut()
        .insert(&REQ_ID, HeaderValue::from_str(&rid).unwrap());

    let start = std::time::Instant::now();
    let mut res = next.run(req).await;
    let elapsed_ms = start.elapsed().as_millis();

    res.headers_mut()
        .insert(&REQ_ID, HeaderValue::from_str(&rid).unwrap());

    tracing::info!(request_id = %rid, status = %res.status().as_u16(), latency_ms = %elapsed_ms, "request finished");

    res
}

Wire it into the router

Add the middleware close to the root router so it applies to every request. This ensures the id is always available, including for error responses.

use axum::{middleware, routing::get, Router};

async fn hello() -> String { "hello".to_string() }

pub fn app() -> Router {
    Router::new()
        .route("/hello", get(hello))
        .layer(middleware::from_fn(request_id_middleware))
}

Integrate with Problem Details

Once you have request ids, errors become much more actionable. A simple improvement is to use the request id as the Problem Details instance field. That way clients can report instance and you can immediately locate logs.

use axum::http::Request;

// Helper that extracts the request id from headers.
pub fn request_id_from_headers<B>(req: &Request<B>) -> String {
    req.headers()
        .get("x-request-id")
        .and_then(|v| v.to_str().ok())
        .unwrap_or("unknown")
        .to_string()
}

At this stage, you can keep this integration simple. Later, you will likely store request id in request extensions for cleaner access from handlers and error mappers.

Production mindset: logs and traces yes, metrics carefully

Request ids are high-cardinality values. They are perfect for logs and traces, but usually a bad fit for metrics labels. Avoid adding request_id as a Prometheus label or similar. Prefer metrics like status code, route name, and error type for aggregation.

Operational verification

Verify behavior in three quick checks:

  • When client sends x-request-id, response echoes the same value.
  • When missing, server generates a new one.
  • Logs include request_id for every request.
# Client-provided request id should be echoed back
curl -i -H "x-request-id: demo-123" http://localhost:3000/hello

# Server should generate a request id when missing
curl -i http://localhost:3000/hello

What comes next

With correlation in place, graceful shutdown becomes the next deployment-oriented baseline. That ensures rollouts do not create noisy connection resets and you can drain traffic safely.