Back to blog
architectureperformancefintechrealtime

Why Rust and Ultimo Are Ideal for Latency-Sensitive Backends: Finance, Bots, and Realtime Services

Ultimo Team10 min read

Not all backend services are created equal. A content delivery API that responds in 50ms is perfectly fine for most use cases. But when a trading bot needs to react to a market event, when an options pricing service must answer before a quote expires, or when a fraud detection pipeline must decide before a payment completes — milliseconds carry real cost.

This post explores why Rust is a strong foundation for these environments, and how Ultimo — built on Hyper 1.x and Tokio — fits naturally into latency-sensitive backend architectures.

The Latency Problem Is Different Here

Most performance discussions focus on throughput: requests per second, concurrent connections, max QPS under load. These are important metrics for high-traffic consumer APIs. For latency-sensitive backends, the relevant metric shifts to tail latency — specifically p99 and p999 response times.

A trading system that averages 0.5ms but occasionally spikes to 250ms has not solved the latency problem. That spike is the problem. Each high-latency event is a missed execution window, a stale quote, or a failed arbitrage opportunity.

Tail latency spikes happen for several reasons:

  • Garbage collection pauses — languages with a GC (Go, JVM, Python) periodically stop the world or pause individual goroutines/threads to reclaim memory. These pauses are unpredictable and show up directly in tail latency.
  • Memory allocation pressure — frequent heap allocations slow down any runtime, even managed ones.
  • Scheduling jitter — OS scheduler preemption adds variance to response times.
  • Sync I/O blocking — any blocking I/O call in a hot path can stall an entire thread pool.

Rust addresses all of these at the language and runtime level.

Why Rust Reduces Tail Latency

No Garbage Collector

Rust uses a compile-time ownership model instead of a runtime garbage collector. Memory is freed deterministically when values go out of scope — no pauses, no stop-the-world events, no GC tuning required. This produces consistently low tail latency because there is no background process competing for CPU time with your request handling.

This is not a theoretical advantage. It is measurable in production GC pause logs for any JVM or Go service under real load.

Zero-Cost Abstractions

Rust's iterator adapters, async/await, closures, and trait dispatch all compile down to code equivalent in performance to what you would write manually in C. You pay for abstractions at compile time, not at runtime. A Rust async fn does not allocate a new heap object per invocation — the state machine is stored inline and only heap-allocated when explicitly boxed.

For a high-frequency API endpoint called millions of times per second, the difference between zero allocation per call and even a small allocation per call is measurable.

Async I/O With Tokio

Tokio is the async runtime underlying Ultimo. It uses an event-driven, cooperative multitasking model backed by OS-native async I/O (epoll on Linux, kqueue on macOS). A Tokio thread park/unpark cycle is measured in nanoseconds. There is no thread-per-connection overhead; a single Tokio runtime can handle hundreds of thousands of concurrent tasks with fixed thread count.

For network-bound services — which most financial backends are — this matters. Waiting on a database response or an exchange websocket message does not block a thread.

#![forbid(unsafe_code)]

Ultimo's entire codebase carries #![forbid(unsafe_code)]. This is not just a philosophical stance — it means the framework will never introduce undefined behavior through a misused pointer, a data race, or a use-after-free bug. For financial services where correctness under concurrency is a compliance concern, this is a meaningful constraint.


Where Ultimo Specifically Helps

Ultimo is not just Rust — it is a batteries-included web framework built on top of Hyper 1.x + Tokio. Here is what it adds that is directly relevant to latency-sensitive backend work:

Type-Safe Contracts From Rust to TypeScript

In a trading system, API drift is dangerous. If the risk service sends {"quantity": "100"} (string) but the order management system expects {"quantity": 100} (integer), the type mismatch surfaces at runtime — at the worst possible moment.

Ultimo's TypeScript client codegen derives types directly from Rust structs. Add #[derive(TS)] to your models and run ultimo generate — every TypeScript consumer gets types that are structurally identical to your server-side Rust types. There is a single source of truth, enforced at compile time on the Rust side and at the TypeScript type checker level on the client side.

#[derive(Serialize, Deserialize, TS)]
#[ts(export)]
pub struct OrderRequest {
    pub symbol: String,
    pub quantity: f64,
    pub side: OrderSide,
    pub order_type: OrderType,
}
 
#[derive(Serialize, Deserialize, TS)]
#[ts(export)]
pub enum OrderSide {
    Buy,
    Sell,
}

Any TypeScript dashboard, bot controller, or internal tool consuming this API will get:

interface OrderRequest {
  symbol: string;
  quantity: number;
  side: "Buy" | "Sell";
  order_type: OrderType;
}

No hand-maintained types. No drift.

JSON-RPC for Internal Services

For internal service-to-service communication — the latency-critical path — REST over HTTP/1.1 carries overhead: headers, content negotiation, URL routing. JSON-RPC over a persistent connection reduces this to a framing concern.

Ultimo supports both REST and JSON-RPC in the same application. You can expose a REST API for external consumers while using typed JSON-RPC for internal service calls:

let mut rpc = RpcRegistry::new();
 
rpc.query("getPosition", |input: PositionQuery| async move {
    let position = db::get_position(&input.account_id, &input.symbol).await?;
    Ok(position)
});
 
rpc.mutation("submitOrder", |input: OrderRequest| async move {
    let order = exchange::submit(input).await?;
    Ok(order)
});

The generated TypeScript client wraps these as async functions with full type inference — no URL construction, no response parsing ceremony.

WebSocket Pub/Sub for Market Data

Market data feeds, trade confirmations, position updates, and risk alerts are fundamentally push-based. Polling a REST endpoint for these is wasteful and introduces latency proportional to your polling interval.

Ultimo includes RFC 6455 WebSocket support with built-in pub/sub channels:

app.websocket("/market-data/:symbol", |ctx: Context| async move {
    let symbol = ctx.param("symbol")?;
 
    ctx.subscribe(format!("quotes.{symbol}")).await;
    ctx.on_message(|msg| async move {
        // handle client messages (subscriptions, unsubscribes)
    }).await
});

Your market data ingestion layer publishes to channels; connected clients receive updates immediately. No polling. No intermediate message broker required for simple fan-out scenarios.

Sessions and Auth Without Boilerplate

Authentication and authorization are solved problems that nonetheless consume significant engineering time when assembled from scratch. Ultimo ships JWT auth, API-key auth, and session management as integrated middleware:

let app = Ultimo::new()
    .with(JwtAuth::new(jwt_secret))
    .with(RateLimiter::new(100, Duration::from_secs(1)));

For a trading API, this means you can add per-key rate limits (protecting downstream exchange rate limits), JWT validation for internal services, and session management for a web dashboard — without writing integration glue.


Architecture Patterns for Latency-Sensitive Backends

Here are three patterns where Ultimo fits well:

1. Market Data Gateway

Exchange WebSocket → Rust ingestion process
    ↓ publish to channels
Ultimo WebSocket server (pub/sub channels)
    ↓ push to subscribers
Trading bots (TypeScript/Python/Rust clients)
Dashboard (React + auto-generated TypeScript types)

The Rust ingestion process parses exchange messages and publishes to Ultimo's pub/sub channels. Connected clients receive sub-millisecond-latency updates without polling. The React dashboard gets fully typed real-time data with zero manual type maintenance.

2. Order Management Service

Internal services → JSON-RPC (type-safe, low overhead)
External integrations → REST + OpenAPI
Risk checks → Middleware pipeline (sync, in-process)

Expose a JSON-RPC interface for order submission and position queries to internal services. Expose REST with an OpenAPI spec for external integrations and compliance tooling. Risk checks run as Ultimo middleware — synchronously in the request path, adding microseconds, not milliseconds.

3. Webhook Ingestion Pipeline

Exchange/Broker webhooks → Ultimo REST endpoint
    ↓ validate + parse (Rust, compile-time types)
    ↓ enqueue to background task
    ↓ respond 200 immediately
Background: reconcile positions, trigger alerts

Webhook handlers should be fast. With Ultimo, request parsing and validation happen at zero cost (Rust type system). The endpoint acknowledges immediately and hands off to background work. WebSocket or SSE pushes status updates back to connected clients.


What to Consider When Choosing Rust for Financial Backends

Rust is not the right choice for every context, even in fintech:

Where Rust shines:

  • Hot-path request handling where GC pauses are unacceptable
  • Type safety for financial data models with strict correctness requirements
  • Long-running services where memory stability matters (no leaks, no gradual degradation)
  • Services where unsafe code would be a compliance/audit concern

Where other choices may be faster to ship:

  • Rapid prototyping and algo research (Python, Jupyter)
  • ETL pipelines with complex logic that changes frequently (Python, Spark)
  • Administrative tooling with low performance requirements (Node.js, Go)
  • Teams without Rust experience — the learning curve is real

Rust has a steeper learning curve than Go, Python, or Node.js. The compiler is strict. The ownership model takes time to internalize. For teams that are new to Rust, Ultimo's batteries-included approach reduces the surface area of things to learn — you don't need to assemble a middleware stack from scratch.


Observability in Latency-Sensitive Systems

Low latency is only useful if you can observe and maintain it. Financial backends need more than uptime monitoring — they need per-endpoint latency histograms, request tracing, and alerting on p99 degradation before it becomes a user-visible problem.

Structured Logging

Rust's tracing crate is the standard for async-safe structured logging in Tokio applications. Spans created with #[instrument] propagate through async call chains automatically, giving you full request context in logs without manual plumbing:

use tracing::instrument;
 
#[instrument(skip(db), fields(symbol = %input.symbol))]
async fn handle_quote_request(
    input: QuoteRequest,
    db: &Pool,
) -> Result<Quote> {
    let quote = db::fetch_latest_quote(db, &input.symbol).await?;
    Ok(quote)
}

Every log line emitted inside handle_quote_request automatically carries the symbol field and span ID, making correlation trivial.

Latency Histograms With OpenTelemetry

For production financial services, export metrics to your observability backend (Prometheus, Datadog, Grafana Cloud) via the OpenTelemetry SDK for Rust:

use opentelemetry::metrics::MeterProvider;
 
let meter = global::meter("order-service");
let request_duration = meter
    .f64_histogram("http.request.duration")
    .with_unit("ms")
    .build();

Record histogram values per endpoint, then build dashboards that surface p50/p95/p99 latency. For a trading system, a p99 above your SLA threshold should trigger an alert before it affects executions.

Health Checks and Readiness Probes

Ultimo makes it straightforward to expose health and readiness endpoints:

app.get("/health", |ctx: Context| async move {
    ctx.json(json!({"status": "ok"})).await
});
 
app.get("/ready", |ctx: Context| async move {
    // check DB connectivity, exchange connection status, etc.
    let db_ok = db::ping(&pool).await.is_ok();
    let exchange_ok = exchange::is_connected();
 
    if db_ok && exchange_ok {
        ctx.json(json!({"status": "ready"})).await
    } else {
        ctx.status(503).json(json!({"status": "not_ready"})).await
    }
});

In a Kubernetes deployment, these drive liveness and readiness probes — ensuring traffic is only routed to instances that are actually connected to downstream systems.


Deployment Considerations

Single Binary Deployment

Ultimo compiles to a single static binary with no runtime dependencies. No JVM heap to tune, no Node.js runtime to manage, no Python interpreter version conflicts. The binary includes your entire application: HTTP server, middleware, business logic, and static assets if you use Ultimo's static file serving.

For financial services, this simplifies deployment, auditing, and reproducibility. The artifact that passed your QA environment is the same binary you deploy to production.

# Multi-stage build — final image is minimal
FROM rust:1.86 AS builder
WORKDIR /app
COPY . .
RUN cargo build --release
 
FROM debian:bookworm-slim
COPY --from=builder /app/target/release/trading-service /usr/local/bin/
CMD ["trading-service"]

Final image size is typically under 50MB — faster pulls, smaller attack surface.

Resource Sizing

Because Rust has no garbage collector and Tokio uses a fixed thread pool (default: one thread per CPU core), resource usage is predictable and stable. There are no JVM heap size parameters, no GC throughput tradeoffs, no "warm-up" period where the JIT needs to compile hot paths.

A well-written Rust service running on Ultimo typically:

  • Uses a small, stable RSS (resident set size) — no gradual memory growth
  • Shows flat CPU usage under stable load — no GC spikes
  • Starts in milliseconds — no class loading or JIT warm-up

For on-call teams and auto-scaling systems, predictable resource usage is operationally valuable.

Rate Limiting Exchange APIs

Most exchanges enforce strict rate limits on order submission and market data APIs. Violating them results in temporary or permanent IP bans. Ultimo's built-in rate limiter can be applied per-user, per-API-key, or globally:

let app = Ultimo::new()
    .with(
        RateLimiter::new(10, Duration::from_secs(1))  // 10 req/s per client
    );

This is particularly useful for services that fan out to exchange APIs — you can enforce your own internal limits that keep you safely below exchange thresholds.


Running Your Own Benchmarks

We do not publish specific req/s numbers for Ultimo because they depend heavily on hardware, workload shape, payload size, network topology, and what else is running on the machine. Published benchmark numbers from framework authors are often best-case figures that don't translate to your production environment.

The right approach is to benchmark in your own environment with your actual workload:

  1. Use wrk or drill for HTTP load testing
  2. Use oha for latency-focused testing (p99, p999 histograms)
  3. Profile with flamegraph to find actual bottlenecks
  4. Compare against your current baseline, not against other frameworks in different environments

The benchmark examples in the Ultimo repository include a server setup and scripts to run against other frameworks in identical conditions.


Summary

Rust's ownership model, zero-cost abstractions, and Tokio's async I/O produce predictable low-latency characteristics that are structurally difficult to achieve in GC-based languages. For financial backends, trading infrastructure, real-time notification pipelines, and bot execution services, this is a meaningful foundation.

Ultimo adds the developer experience layer on top: type-safe TypeScript codegen, JSON-RPC, WebSocket pub/sub, integrated auth, and a zero-unsafe policy. For teams building these systems, it reduces the boilerplate of assembling these components from scratch while staying close to the metal.

If your backend's correctness and tail latency directly affect revenue, Rust and Ultimo are worth a serious evaluation.

Next steps: