BeCuriouzLearn something new every day
How Kotlin runs a million coroutines on a phone
💡 Coroutines are cheap because suspending pauses your function without blocking the thread it was running on — so one thread can serve thousands of waiting tasks.
A JVM thread is expensive: each one grabs a chunk of stack memory, and the OS has to context-switch between them. Try to spin up 50,000 real threads and you can burn ~100 GB of memory. Yet Android apps happily juggle thousands of concurrent network calls, database reads, and animations. The trick is that coroutines aren't threads at all — 50,000 of them fit in roughly 500 MB.
The key word is suspend. When you mark a function suspend fun loadUser(), you're telling the compiler this function can pause partway through — say, while waiting for a network response — and resume later. Crucially, when a coroutine suspends, it does not block the underlying thread. The thread is handed back to run other coroutines. That's why one thread can service hundreds of in-flight operations: they're almost never all running at once, just waiting.
Under the hood the compiler rewrites your suspend function into a state machine. Each suspension point becomes a saved checkpoint (a Continuation) holding local variables and where to resume. There's no magic OS thread parking — just an object on the heap. A suspend function can only be called from another suspend function or a coroutine builder like launch, which is how the compiler guarantees a valid resume point always exists.
This is why blocking calls are the classic beginner mistake: writing Thread.sleep(1000) inside a coroutine freezes the whole thread, defeating the point. The coroutine-friendly delay(1000) suspends instead — the thread stays free for everyone else.