When we started building TRCR, we had a choice to make: what language would power our backend? We needed something that could handle real-time WebSocket connections at scale, serve REST and GraphQL APIs simultaneously, and run background jobs for invoice generation and notification delivery — all without falling over under load.
We chose Rust. A year later, that decision has shaped every part of our architecture, and we haven't looked back. Here's why.
The Problem We Were Solving
TRCR isn't a typical CRUD app. At its core, it's a real-time collaboration platform. When a team member starts a timer, every other member on the dashboard needs to see that update instantly. When someone sends a chat message, it needs to arrive in milliseconds. When a task status changes, the Kanban board needs to reflect it across all connected clients.
This means our server is maintaining thousands of persistent WebSocket connections simultaneously, each of which can receive messages at any time. On top of that, we're serving REST endpoints for the HTTP fallback layer, a GraphQL API for flexible queries, and running background jobs for things like PDF invoice generation and overdue invoice detection.
We needed a language that could handle this concurrency efficiently without requiring an army of servers.
Why Not Node.js or Go?
We seriously considered both. Node.js with its event loop is natural for WebSocket-heavy workloads, and the JavaScript ecosystem is massive. Go has excellent concurrency primitives with goroutines and a fast garbage collector.
The concerns with Node.js were twofold: single-threaded CPU-bound work (PDF generation, report calculations) would block the event loop unless we offloaded everything to worker threads, adding complexity. And while V8 is fast, its memory overhead per connection is significantly higher than what we could achieve with Rust.
Go was a closer call. Goroutines are lightweight and the language is simple. But Go's garbage collector introduces latency spikes that are hard to predict, especially under high memory pressure. For a real-time system where consistent sub-10ms response times matter, GC pauses were a concern. We've seen Go services in production where P99 latency jumps 10x during a GC cycle.
What Rust Gives Us
Zero-Cost Abstractions
Rust's ownership model means we get memory safety without a garbage collector. No GC pauses, no unpredictable latency spikes. Our P99 latency is consistently under 5ms for WebSocket message delivery, even during peak hours with thousands of concurrent connections.
Fearless Concurrency
Rust's type system prevents data races at compile time. When you're managing thousands of WebSocket connections that share state (like “who's currently tracking time?”), this is invaluable. In other languages, you discover concurrency bugs in production at 3 AM. In Rust, the compiler catches them before you push.
Tokio and the Async Ecosystem
The Tokio async runtime gives us a multi-threaded, work-stealing scheduler that efficiently multiplexes thousands of async tasks across CPU cores. Combined with Axum (our HTTP framework, also from the Tokio team), we get a production-grade web server that handles HTTP/1.1, HTTP/2, and WebSocket upgrades out of the box.
Our server architecture looks like this:
// Single binary serves everything
let app = Router::new()
.nest("/api", rest_routes)
.nest("/graphql", graphql_routes)
.route("/ws", get(websocket_handler))
.layer(cors_layer)
.layer(auth_layer);
// One process, all interfaces
axum::serve(listener, app).await?;One binary. One process. REST, GraphQL, and WebSocket all running on the same server, sharing the same connection pool and in-memory state. No microservice orchestration, no inter-service communication overhead.
Memory Efficiency
Our production server handles 5,000+ concurrent WebSocket connections while using under 200MB of RAM. An equivalent Node.js server with the same connection count would easily consume 1-2GB. This means we can run on smaller (cheaper) instances and still have headroom.
SeaORM for Database Access
We use SeaORM as our database layer, which gives us type-safe queries against PostgreSQL. Every query is checked at compile time — if we rename a column or change a type, the compiler tells us every callsite that needs to be updated. This has prevented dozens of runtime errors that would have been subtle bugs in a dynamically-typed language.
The Tradeoffs
Rust isn't free. There are real costs:
- Steeper learning curve. The borrow checker is famously challenging for newcomers. Our first few months had longer development cycles as the team internalized ownership semantics.
- Slower iteration speed. Compile times are longer than Go or TypeScript. We mitigate this with incremental compilation and
cargo watch, but it's still noticeable. - Smaller talent pool. Finding Rust engineers is harder than finding Node.js or Go engineers. We've found that strong C++ or systems programming backgrounds translate well.
- Ecosystem maturity. While the async ecosystem is excellent, some libraries are less mature than their Node.js or Go equivalents. We've contributed patches upstream to a few crates.
The Results
After a year in production, here's where we stand:
- P50 WebSocket latency: 1.2ms. P99: 4.8ms. Consistent, with no GC-induced spikes.
- 5,000+ concurrent connections on a single 2-vCPU, 4GB instance.
- Memory usage: 180MB under full load. The binary itself is 12MB.
- Zero memory-related production incidents. No leaks, no segfaults, no OOM kills.
- Background jobs (invoice PDFs, overdue detection, git sync, notifications) run in the same process via Tokio tasks, sharing the database pool.
Key Takeaway
If your product is I/O-heavy (WebSockets, database queries, HTTP calls) with occasional CPU-bound work, and you care about predictable latency and low memory footprint, Rust with Tokio and Axum is an excellent choice. The upfront investment in learning the language pays off in operational simplicity and performance.
Would We Choose Rust Again?
Without hesitation. The confidence we get from the type system and borrow checker means we can ship features faster with fewer bugs. The performance means we can serve our entire user base from a small cluster. And the reliability means we spend our nights sleeping instead of firefighting.
If you're building a real-time, multi-interface platform and you're evaluating backend technologies, give Rust a serious look. The learning curve is real, but the payoff is worth it.