Kotlin

What Are Kotlin Coroutines? A Simple Overview

What Are Kotlin Coroutines? A Simple Overview

Kotlin coroutines let code wait for a network response, a database row, or a timer without blocking the thread it runs on. A suspended task steps aside, the thread goes off and does other work, and the task continues from the same spot later.

Part of the feature lives in the language. The Kotlin compiler handles suspend and resume. Everything else you use day to day (coroutine builders, dispatchers, the structured concurrency tools) comes from kotlinx.coroutines, a separate library that JetBrains ships and versions on its own.

It’s usually described as a concurrency design pattern for asynchronous programming, and Google recommends it as the default approach for asynchronous work on Android. On the JVM, teams compare it with plain Java threads. That comparison mostly comes down to memory cost and switching overhead, which decide whether an app can handle thousands of concurrent tasks.

Kotlin first shipped the pattern as an experimental, opt-in feature in version 1.1, released in March 2017. The stable release came later, in Kotlin 1.3 (JetBrains, 2017).

What Are Kotlin Coroutines?

YouTube player

A coroutine is not a thread. That confuses a lot of people at first. It runs on top of a thread, and a single thread can hold thousands of suspended coroutines at once, pausing and resuming each one instead of blocking it.

The split between language and library confuses people too. The suspend keyword belongs to Kotlin itself and is compiled directly by the language. When the compiler sees it, it rewrites the function into a resumable state machine. The builders and dispatchers that put it to work (Flow too) come from kotlinx.coroutines, which JetBrains develops on its own release schedule.

Coroutines reached stable release in October 2018, when Kotlin 1.3 shipped alongside kotlinx.coroutines 1.0 (JetBrains, 2018).

They work for non-blocking code on JVM, JS, and Native targets. Android made them popular, but they aren’t an Android thing. They also aren’t a Java language feature. The JVM went a different way and added virtual threads, which is a separate model. And they don’t replace OS threads, since they still need real threads underneath to run on.

Is the switch worth it for a team? Google’s Android guidance says yes, and has a number for it. Over half of professional developers who use coroutines report a productivity increase after adopting them (Android Developers, citing Google internal data, 2020). It’s self-reported, so read it that way. It does match what most Android devs who’ve made the move will tell you.

How Do Kotlin Coroutines Work?

YouTube player

The compiler does most of the work. It turns each suspend function into code that can stop halfway and come back later, and it tracks where it stopped in a hidden object called a continuation.

Why is Kotlin becoming the new Java?

Discover Kotlin statistics: Android adoption, multiplatform growth, developer satisfaction, and the modern language evolution from JetBrains.

Explore Kotlin Data →

At a suspension point, the coroutine gives control back to the thread. Later it resumes from that same place.

Suspend Functions and Continuations

Marking a function with suspend tells the compiler this function may pause and resume without blocking whatever called it.

Under the hood, the compiler adds one extra parameter to every suspend function, which is the continuation. The technique is called continuation passing style.

The continuation holds the local variables and the exact line where execution stopped. So resuming a coroutine means calling the continuation, not running the function again from the top. No new operating system thread gets created anywhere in this.

That’s also why you can’t call a suspend function from ordinary code. It won’t compile. A regular function has no continuation to pass along.

Compiler-Generated State Machines

Each suspend function compiles into a state machine, with roughly one state per suspension point in its body. A function with three delay calls ends up as something close to four states, and it jumps between them as each suspension resolves.

Why go to all this trouble? Memory, mostly.

The current Kotlin documentation has a demo where 50,000 coroutines each wait five seconds and then print a dot. Run the same demo with 50,000 JVM threads and it can need up to 100 GB of memory. The coroutine version needs roughly 500 MB (Kotlin documentation, 2026).

If you want to see the state machine for real, DoorDash’s engineering team decompiled coroutine bytecode and walked other engineers through it line by line (DoorDash Engineering blog). Worth reading once. You won’t need it on a normal workday.

What Are Coroutine Builders in Kotlin?

Since a suspend function can’t be called from regular code, something has to start the first coroutine. Builders do that. In practice you’ll use launch, async, and runBlocking for nearly everything, and each one hands back a different kind of handle.

Looking to sharpen your Kotlin skills? Data classes, extension functions, null safety, and everything else you need - including let, apply, and scope functions - is on one page in the Kotlin Cheat Sheet.

BuilderReturnsBlocks callerTypical use
launchJobNoFire-and-forget work, UI updates
asyncDeferredNoConcurrent work that returns a value
runBlockingThe block’s resultYesBridging blocking code to suspend functions

launch starts a coroutine and returns a Job right away. You use that Job later to cancel the coroutine or wait for it to finish. No return value comes back. It’s for running something in the background and moving on.

With async you get a Deferred, and calling await on it suspends until the value is ready. The reason developers pick async over launch is concurrency. Start two async calls before awaiting either one and they run at the same time.

runBlocking is different from the other two. It blocks the calling thread until its coroutine finishes.

That’s fine in a main function or a quick test. Inside Android UI code it’s a common mistake, and it cancels out the whole reason for using coroutines.

The builder API has barely changed since Kotlin 1.3. The current kotlinx.coroutines release, version 1.11.0, published to Maven Central in May 2026, still exposes the same three core builders developers reached for eight years earlier (JetBrains, kotlinx.coroutines release history, 2026).

What Is a Coroutine Scope in Kotlin?

YouTube player

Every coroutine builder needs a CoroutineScope to run in. The scope owns the coroutine’s lifetime, and once the scope is cancelled, every coroutine launched inside it gets cancelled too.

This requirement is how Kotlin enforces structured concurrency. Coroutines don’t get to run loose.

GlobalScope vs Structured Scopes

GlobalScope is the exception. A coroutine started there is tied to the whole application’s lifetime, and there’s no parent around to cancel it automatically.

JetBrains marked GlobalScope with the DelicateCoroutinesApi annotation, so the compiler now warns you before it lets you use it at all.

The warning makes sense. Nothing cancels a GlobalScope coroutine when a screen or component gets destroyed, so it’s an easy way to leak memory if the coroutine outlives its purpose. Real app-wide background work is a valid use. It’s just rare.

Structured scopes work the other way around. A parent job tracks every child, and cancelling the parent cancels the whole job hierarchy in one call.

Android Scope Extensions

Android’s Jetpack libraries add viewModelScope and lifecycleScope on top of the core API.

viewModelScope belongs to a ViewModel and gets cancelled automatically when the ViewModel clears. For Activities and Fragments you have lifecycleScope, which follows the component’s app lifecycle and stops once that component is destroyed.

Home Assistant’s Android team had a Wear onboarding flow built on raw threads. They rewrote it with lifecycleScope coroutines cancelled inside onDestroy to stop a recurring Play Console crash (GitHub, 2026).

That one change removed the manual thread bookkeeping the old code depended on.

What Are Kotlin Coroutine Dispatchers?

YouTube player

When a coroutine resumes from suspension, something has to decide which thread, or pool of threads, it runs on. That’s the dispatcher. Kotlin has a handful built in, and picking the wrong one causes a lot of bugs in Android development.

Dispatchers.Main runs coroutines on the UI thread. Anything that touches a view directly needs it.

For blocking calls, use Dispatchers.IO. That covers file reads, database queries, network requests, pretty much anything that sits and waits on something outside the CPU.

The official API reference caps Dispatchers.IO at 64 threads or the number of CPU cores, whichever is larger, with extra threads created and shut down on demand (kotlinx.coroutines API documentation).

Dispatchers.Default is for CPU-heavy work, like sorting a big list or parsing a large JSON file. It caps out at the number of CPU cores on the machine, with a minimum of two threads even on single-core hardware (kotlinx.coroutines API documentation).

Then there’s Dispatchers.Unconfined. It starts in the caller’s thread and only switches after the first suspension point. Most guides recommend keeping it out of production code, and I agree with them.

To switch dispatchers partway through a coroutine, use withContext. It suspends on the current dispatcher and resumes on the new one, and the coroutine never leaves the scope that owns it.

How Does Coroutine Cancellation Work in Kotlin?

YouTube player

Cancellation is cooperative. Nothing kills a coroutine from the outside. The coroutine has to check for cancellation and stop itself.

Calling cancel on a Job sets a flag and throws a CancellationException at the coroutine’s next suspension point. A tight loop with no suspension point never notices. This one catches people all the time.

Here’s what happens, in order, when you cancel a coroutine:

  1. Call cancel on the Job or the scope that owns the coroutine
  2. The coroutine’s isActive flag flips to false
  3. A CancellationException fires at the next suspension point, such as delay or yield
  4. Any try/finally block inside the coroutine still runs, so cleanup happens
  5. The Job moves to a cancelled state, and join returns right away for anyone waiting on it

Loops that never suspend need a manual check. Read isActive directly, or call ensureActive, which throws immediately once the Job is cancelled.

Another common mistake is a broad catch block that catches CancellationException and swallows it. That breaks structured concurrency, because the parent never learns the child stopped. Rethrow it.

Cleanup code that itself needs to suspend, like closing a network connection, has to run inside withContext(NonCancellable). Otherwise it gets cancelled before it finishes.

How Are Exceptions Handled in Kotlin Coroutines?

By default, an uncaught exception inside a coroutine cancels its parent. That cancellation then spreads to every sibling coroutine in the same job hierarchy.

launch and async don’t fail the same way. With launch, the exception goes straight to the thread’s default exception handler. async holds onto it until someone calls await.

CoroutineExceptionHandler

This handler only catches exceptions that have no propagation path left. Usually that’s a root coroutine in GlobalScope, or a child of a SupervisorJob.

So it works on GlobalScope.launch and GlobalScope.async as root coroutines, and on children of a supervisorScope or a SupervisorJob. On ordinary structured children it’s ignored, because that exception already has somewhere to go (its parent). Most developers are surprised by this the first time they hit it.

It can’t save the coroutine, either. By the time the handler runs, the coroutine has already completed. It behaves like Thread.uncaughtExceptionHandler on the JVM, a last stop for logging rather than a way to resume anything.

SupervisorJob and Failure Isolation

With a regular Job, one failed child cancels every sibling. A SupervisorJob doesn’t do that. Each child under it fails on its own, and the rest of the hierarchy keeps running.

This matters most in UI code. One failed network call shouldn’t cancel three other coroutines updating unrelated parts of the screen.

If you only need isolation for a single block of code, supervisorScope gives you the same behavior without wiring up a SupervisorJob by hand.

The Home Assistant fix mentioned earlier also narrowed its exception handling to ApiException instead of catching every throwable, so unrelated failures in the Wearable data calls stopped being masked (GitHub, 2026).

Kotlin Coroutines vs Threads: What Is the Difference?

YouTube player

Both run code concurrently. The difference is what each unit costs.

Because a coroutine suspends instead of blocking, a small pool of real threads can carry thousands of coroutines. You don’t need one thread per task.

AspectOS threadKotlin coroutinePractical limit
Memory per unit512 KB to 1 MB stack, JVM defaultA few hundred bytes to a few KBThousands of threads risk OutOfMemoryError
Switching costKernel-level context switchUser-space suspension, no kernel callCoroutines switch far cheaper at scale
SchedulingHandled by the operating systemHandled by the Kotlin runtimeCoroutines still run on real threads underneath

JVM threads default to a 512 KB or 1 MB stack depending on the platform (Oracle JRockit and OpenJDK documentation). An independent Android benchmark measured roughly a 6:1 memory ratio between a thread and a coroutine for IO-bound work (TechYourChance benchmark, 2023 to 2024).

That same benchmark found something else worth knowing. Bare threads and a fixed thread pool consumed almost identical memory, which confirms the coroutine advantage sits in IO suspension specifically.

Scheduling is the other big difference. The operating system schedules threads through preemptive multitasking, while the Kotlin runtime decides when a suspended coroutine resumes.

And the question people always ask about the underlying thread has one honest answer. Coroutines still run on real OS threads. They just don’t need one each.

Kotlin Coroutines vs RxJava: Which Should You Choose?

YouTube player

Both handle asynchronous work, with different mental models. Coroutines read like sequential code. RxJava has you compose chains of operators over event streams.

The New York Times migrated core libraries and parts of its News app from RxJava to Kotlin coroutines and Flow, citing simpler onboarding for its Android team (New York Times Android engineering talk, GDG Dallas).

Plenty of other codebases never migrate outright. They run both side by side through the kotlinx-coroutines-rx3 interop library.

The case for coroutines is mostly readability. Because the code looks sequential, teams new to async work get up to speed faster. It’s also built into Kotlin and Jetpack directly, so there’s no separate operator vocabulary to learn. Where it falls short is complex multi-source stream composition, which a mature operator library still handles better.

RxJava has been around longer, and it shows:

  • A large, battle-tested operator set for debouncing, throttling, combining and merging streams
  • Still actively maintained, with RxJava3 reaching version 3.1.12 on Maven Central in September 2025

The learning curve is steeper, though. Observables and subscription lifecycles take a while to get right.

Teams starting a new Android project default to coroutines and Flow first, and reach for RxJava only when one specific operator earns its added complexity. That’s what I’d do too.

How Do Kotlin Coroutines Work With Kotlin Flow?

YouTube player

A suspend function returns one value. Flow is a stream type built on coroutines that emits many values over time.

In a Kotlin Flow, every emission moves through the same coroutine that started the collection. map, collect, and every other operator are suspend functions themselves.

The two specializations you’ll run into most are StateFlow, which holds current state, and SharedFlow, which broadcasts one-off events.

StateFlow for State

A StateFlow always has a value. It starts with an initial one and never stops holding one, since it represents current state and not an event.

New collectors get that current value immediately. State flow always replays exactly one value to every new subscriber (kotlinx.coroutines API documentation).

Updates are conflated through equals comparison. A slow collector skips the intermediate values and lands on the latest one.

In most Android apps it’s backed by the stateIn operator or a private MutableStateFlow, then exposed publicly as a read-only StateFlow so the mutable version stays internal to the class.

SharedFlow for Events

SharedFlow is the more general primitive underneath StateFlow. By default it has zero replay and zero buffer. The full default configuration is replay equals zero, extraBufferCapacity equals zero, and onBufferOverflow suspends the emitter (kotlinx.coroutines API documentation).

So an event emitted with no active collector is lost for good. For a one-time snackbar message, that’s exactly the behavior you want.

Square’s Cash App exposes SQLDelight database queries as a Flow through its coroutines extension, so a query’s Flow re-emits automatically the moment the underlying table changes (Cash App engineering blog).

How Do You Use Kotlin Coroutines in Android Development?

YouTube player

The setup looks about the same on every screen. Most bugs come from skipping the scope part and reaching for GlobalScope out of old habit.

From dependency to cleanup, it usually goes like this:

  1. Add the kotlinx-coroutines-android dependency alongside kotlinx-coroutines-core
  2. Launch coroutines from viewModelScope or lifecycleScope instead of GlobalScope
  3. Switch to Dispatchers.IO inside withContext for any network or database call
  4. Collect the result back on the main thread and update UI state through StateFlow
  5. Let the scope’s built-in cancellation handle cleanup once the screen closes

Common Mistakes and Debugging Coroutine Leaks

The mistake you’ll see most is holding a reference to a coroutine started in a scope that outlives the screen. Usually that’s a launch in GlobalScope, done out of habit.

Android Studio and IntelliJ IDEA both support a coroutine debugger. You turn it on with the -Dkotlinx.coroutines.debug VM option, and it lists every running coroutine and its current state. The debug agent also adds a readable coroutine number to log output, so you can trace individual coroutines across suspensions.

Low-tech checks help too:

  • Logging thread names is the simplest way to confirm which dispatcher a suspicious coroutine ran on
  • A leaked coroutine usually shows up first in the memory profiler, as a destroyed Activity or Fragment that’s still being retained

The fix is almost always the same. Move the launch call out of GlobalScope and into viewModelScope or lifecycleScope.

When Do Kotlin Coroutines Not Apply?

In some situations coroutines just add ceremony and don’t help. It’s worth knowing where those are.

Pure CPU-bound work is the obvious case. Sorting, parsing, or heavy computation gets nothing from suspension, since there’s no waiting to suspend during.

The same independent benchmark that measured coroutine memory savings for IO-bound tasks didn’t test CPU-bound work directly. Its author expects CPU-bound coroutines on Dispatchers.Default to converge to roughly the same footprint as an equivalent thread pool, since raw throughput is capped by physical cores either way.

Legacy blocking Java libraries are another. A library with no suspend-compatible API still blocks whatever thread calls it, coroutine or not. Wrapping it with suspendCancellableCoroutine helps callers, but the blocking call underneath still ties up a real thread until it returns.

If your codebase is plain Java, you have a real alternative now. Java’s own virtual threads, finalized in JDK 21, offer a similar lightweight-thread model without a suspend-based rewrite (OpenJDK, JEP 444, 2023).

Short-lived scripts rarely need this either. A five-line command-line tool doesn’t need structured concurrency’s scope management, and wrapping it in runBlocking adds ceremony for no real gain.

Coroutines also have failure modes of their own. A mismanaged scope still leaks memory, so they aren’t automatically safe from that. A tight loop with no suspension point ignores cancellation until you add a manual check. Tracing a stack across suspension points is also harder to read than a single blocking call, even with a coroutine debugger attached (that one still annoys me).

None of this makes coroutines a poor default for Android or Kotlin backend work.

The pattern solves one specific problem, waiting without blocking, and stops helping once that problem is gone.

FAQ on What Are Kotlin Coroutines

Who Created Kotlin Coroutines and When Did They Reach Stable Release?

JetBrains engineer Roman Elizarov led the design, which went through the company’s official KEEP process.

Coroutines showed up as experimental in Kotlin 1.1 back in 2017. They became stable in Kotlin 1.3, in October 2018.

What Is Dispatchers.Unconfined Used For?

It starts a coroutine in the caller’s current thread and only switches after the first suspension point.

That makes it handy for unit tests and rare cases that need immediate execution. In everyday production code, predictable threading matters more.

What Is the Difference Between StateFlow and LiveData?

LiveData came first. It’s Android’s older lifecycle-aware observable, built before coroutines existed.

StateFlow is coroutine-native and works outside Android in Kotlin Multiplatform code. The catch is lifecycle handling, which you now do manually and LiveData used to do for you.

How Do Kotlin Coroutines Compare to Java Virtual Threads?

Virtual threads run ordinary blocking Java code unchanged, and the JVM schedules them cheaply.

Coroutines need suspend functions and a compiler rewrite. In return, they reach JS and Native targets, which virtual threads, a JVM-only feature, never touch.

How Do You Unit Test Kotlin Coroutines?

Use the kotlinx-coroutines-test library. It provides runTest and a TestDispatcher that skips real delays instead of waiting in real time.

Tests stay fast and deterministic, and StandardTestDispatcher lets you control exactly when queued coroutines execute.

Can Kotlin Coroutines Be Used Outside Android?

Yes. They run anywhere Kotlin compiles: backend servers with Ktor or Spring, desktop tools, and Kotlin Multiplatform projects sharing logic between Android and iOS.

Android popularized the pattern, but the JVM, JS, and Native targets support it just as well.

What Should You Fix First in Kotlin Coroutines?

YouTube player

Fix scope leaks first, before dispatcher choice or exception handling. A leaked scope keeps failed work running long after a screen closes, and it hides every other issue behind it.

Dispatcher misuse comes next, which mostly means moving blocking calls off Dispatchers.Main. Silent cancellation goes last, once broad catch blocks stop swallowing CancellationException.

Together these account for most production coroutine bugs. Fixing them out of order means you spend debugging time on symptoms instead of causes.

Yes, this pushes dispatcher-level performance tuning further down the list. It’s a trade worth making, because a leaked scope crashes an app long before a suboptimal thread pool does.

The same order works in Kotlin multiplatform projects, where suspend functions, scopes, and cancellation behave identically on iOS and Android.

Bogdan Sandu

Stay sharp. Ship better code.

Every week: one curated article, one tool worth knowing, one tip you can use tomorrow. No noise, no padding.