Rust vs Kotlin

For Backend Development

Choose Rust for high-performance native backends or Kotlin for rapid JVM-based development and Android integration.

The choice between control and velocity

You're building a backend service. You have two tools on the table. One feels like a precision instrument made of titanium. The other feels like a Swiss Army knife that happens to run on a very reliable engine. You're not choosing between good and bad. You're choosing between different kinds of fast and different kinds of safe.

Rust compiles to native code. It owns its memory. It gives you zero-cost abstractions and a compiler that refuses to let you ship code with data races or null pointer dereferences. Kotlin runs on the JVM. It relies on garbage collection. It lets you write code at the speed of thought and leverages a massive ecosystem of libraries.

The decision isn't about which language is superior. It's about what your backend needs to do and what your team can tolerate while building it.

How they handle memory

Rust uses ownership. Every value has exactly one owner. When the owner goes out of scope, the value is dropped immediately. There is no background process cleaning up memory. There is no pause while the runtime scans the heap. You allocate, you use, you deallocate. The compiler enforces the rules so you never leak and never use-after-free.

Kotlin uses references. You create objects, and they live on the heap. The garbage collector runs periodically to find objects that are no longer reachable and reclaim their memory. This frees you from manual memory management. It also introduces non-determinism. You don't know exactly when memory is reclaimed. In a high-throughput service, this can cause latency spikes when the GC kicks in.

Think of Rust like driving a manual transmission sports car. You control the gears. You coordinate the clutch and the gas. If you mess up, you stall or blow the engine. But you get maximum efficiency and total control. Kotlin is like a high-end automatic. You focus on the road. The car handles the gears. It might use a bit more fuel, and there's a tiny delay when it shifts, but you can drive it comfortably for hours without fatigue.

Rust makes you pay for safety upfront. Kotlin makes you pay for safety in runtime overhead.

Minimal examples

Here's the bare minimum to get a backend running in both languages. The syntax looks different, but the intent is the same.

// Rust: Native binary, zero runtime overhead
// Tokio is the async runtime. It handles the event loop and thread pool.
#[tokio::main]
async fn main() {
    // No hidden allocations. This prints directly to stdout.
    println!("Rust backend running");
}
// Kotlin: Runs on JVM, requires JRE or GraalVM native image
// The JVM handles memory management automatically.
fun main() {
    // String creation involves heap allocation and GC pressure.
    println("Kotlin backend running")
}

Rust code requires an async runtime like Tokio to handle concurrency efficiently. The runtime is explicit in your dependencies. Kotlin code runs on the JVM, which provides the runtime implicitly. You don't need to add a dependency for the basic execution model, but you do need the JVM installed on your server.

Convention aside: Rust developers run cargo clippy before every commit. It's a linter that catches idiomatic mistakes and suggests improvements. It's not just a style checker; it catches logic errors. Treat clippy warnings as errors in your CI pipeline. In Kotlin, you typically use ktlint or rely on IDE inspections, but the tooling is less standardized across the community.

What happens under the hood

When you compile Rust, you get a binary that runs directly on the CPU. There is no virtual machine. There is no garbage collector waking up to pause your threads. The binary contains all the machine instructions needed to execute your logic. Startup time is fast. Memory footprint is small. You can run hundreds of Rust services on a single node without running out of RAM.

When you compile Kotlin, you get bytecode that runs on the JVM. The JVM loads the bytecode, verifies it, and compiles hot paths to native code using Just-In-Time (JIT) compilation. This means the code gets faster as it runs, but there's a warm-up period. The JVM also manages a heap. Objects are allocated on the heap, and the GC reclaims them. This adds overhead. The JVM itself consumes memory, even for a simple "Hello World" service.

Concurrency models differ too. Rust has async/await for non-blocking I/O, but it also supports threads. The compiler enforces thread safety with the Send and Sync traits. You can't accidentally share mutable state between threads. If you try, the compiler rejects you. Kotlin has coroutines. Structured concurrency makes it easy to write async code without callback hell. You can suspend functions and resume them later. It's ergonomic, but data races can still happen if you share mutable state without proper synchronization.

Rust forces you to think about the machine. Kotlin lets you think about the business logic. Pick based on what your project demands.

Realistic backend code

Let's look at a realistic scenario: a handler that fetches a user from a database and returns JSON.

use serde::{Deserialize, Serialize};

// Derive macros generate serialization code. No runtime reflection.
#[derive(Debug, Serialize, Deserialize)]
struct User {
    id: u64,
    name: String,
}

// Handler function. No hidden allocations if you're careful.
// The compiler ensures you handle errors explicitly.
async fn get_user(id: u64) -> Result<User, String> {
    // Simulate DB lookup. In real code, this would be async I/O.
    if id == 1 {
        Ok(User { id, name: "Alice".to_string() })
    } else {
        Err("User not found".to_string())
    }
}
// Data class generates constructor, getters, equals, hashCode, toString.
data class User(
    val id: Long,
    val name: String
)

// Suspend function for async. JVM handles the coroutine state machine.
// Null safety is enforced by the type system, but unsafe casts can bypass it.
suspend fun getUser(id: Long): User? {
    // Simulate DB lookup.
    return if (id == 1L) User(1L, "Alice") else null
}

Rust code reads like a contract. The types tell you exactly what can go wrong. The Result type forces you to handle errors. You can't forget to check for a missing user. Kotlin code reads like a conversation. It's concise and expressive. The ? return type signals that the user might be null, and the compiler helps you handle it. But Kotlin allows !! to assert non-null, which can throw a runtime exception if you're wrong.

Rust code is more verbose, but the verbosity is a feature. It documents the constraints of your code. Kotlin code is shorter, but you have to trust the runtime to catch mistakes.

Pitfalls and compiler errors

Rust has a steep learning curve. The borrow checker fights you until you understand ownership. You'll see errors like E0502 (cannot borrow as mutable because it is also borrowed as immutable) when you try to mutate a struct while holding a reference. You'll see E0382 (use of moved value) when you try to use a value after it's been moved into a function. These errors stop you from writing buggy code, but they force you to restructure your logic.

Kotlin has runtime pitfalls. Garbage collection pauses can spike latency. If you allocate millions of short-lived objects, the GC kicks in and your service stalls. You have to tune the heap size and GC parameters. You can also run into dependency hell with Gradle or Maven. Transitive dependencies can conflict, and resolving them takes time.

Null safety in Kotlin is good, but not perfect. You can cast to Any? and lose type information. You can use !! to suppress null checks. These are escape hatches that can lead to runtime crashes. Rust doesn't have escape hatches for null safety. The type system guarantees that a value is either present or absent, and you must handle both cases.

Respect the borrow checker. It's not trying to annoy you; it's trying to save you from a production outage.

Decision matrix

Use Rust when you need deterministic latency and cannot afford garbage collection pauses in your critical path. Use Rust when you are building a service where memory footprint is constrained, such as running many instances on a single node. Use Rust when your team wants the compiler to enforce correctness and eliminate entire categories of runtime bugs. Use Rust when you are building infrastructure like databases, proxies, or compilers where performance and safety are paramount.

Use Kotlin when rapid iteration is the priority and you need to prototype and ship features faster than Rust's learning curve allows. Use Kotlin when your team has Java expertise and you want to leverage the vast ecosystem of JVM libraries and frameworks. Use Kotlin when you are building a standard web service where throughput is important but microsecond-level latency guarantees are not required. Use Kotlin when you need strong Android integration and want to share code between backend and mobile.

There is no universal winner. There is only the right tool for your specific backend.

Where to go next