A coroutine is a function that can suspend its execution at defined points and later resume from the same place with its local state intact. In concurrency, coroutines let one thread interleave many tasks cooperatively, which makes them the foundation of async/await in Python, JavaScript, C#, Rust and Kotlin, and of goroutine-style runtimes.
Where an ordinary subroutine runs from entry to return in one go, a coroutine has multiple entry and exit points. It hands control back voluntarily, usually while waiting for input or output, so the runtime can run something else in the meantime without the cost of an operating system thread switch.
How a coroutine suspends and resumes
The essential operation is the suspension point, written as await, yield or a call to a suspending function. At that point the coroutine saves where it stopped and its live variables, then returns control to a caller or to a scheduler, often an event loop. When the awaited result is ready, the scheduler resumes the coroutine at the saved point.
Because a coroutine gives up control only at these explicit points, scheduling is cooperative rather than preemptive. Code between two suspension points runs without interruption from other coroutines on the same thread, which removes many race conditions but also means a long computation blocks every other task on that thread.
Implementations differ in how they store the suspended state, as the following table shows.
| Model | How state is kept | Can suspend from nested calls | Examples |
|---|---|---|---|
| Stackless | Compiler-generated state machine object | Only through awaited calls | C# async, Rust futures, C++20 coroutines, Python async def |
| Stackful | Separate small stack per coroutine | Yes, from any depth | Lua coroutines, Go goroutines, Java virtual threads |
| Generator-based | Frame object kept by the runtime | Only at the yield in the generator | Python generators, JavaScript generators |
Stackless coroutines are very cheap, often a few hundred bytes each, but every function on the path must be marked async, which spreads the keyword through the codebase. Stackful coroutines avoid this so-called function coloring problem at the cost of more memory per task.
Coroutines compared with threads and related concepts
Coroutines solve a similar problem to threads, running many activities at once, but with a different cost and control model. A thread is scheduled by the operating system and can be interrupted at any instruction, while a coroutine is scheduled by a language runtime or library and switches only where the code allows it.
| Unit | Scheduling | Typical memory per unit | Switch cost |
|---|---|---|---|
| OS thread | Preemptive, by the kernel | Stack of about 1 to 8 MB reserved | Kernel context switch, microseconds |
| Green thread | Preemptive or cooperative, by a runtime | Small growable stack | User-space switch |
| Stackless coroutine | Cooperative, at await points | Hundreds of bytes to a few KB | Function return and call, nanoseconds |
| Actor | Runtime processes one message at a time | Mailbox plus state | Message dispatch |
Coroutines provide concurrency, not parallelism. On a single thread they interleave tasks but never run two at the same instant. Runtimes such as Kotlin dispatchers, Tokio in Rust or the Go scheduler distribute coroutines over a thread pool to add parallelism, which brings back the need for thread safety where coroutines share data.
Where coroutines fit best
Coroutines excel at I/O-bound workloads in which tasks spend most of their time waiting: network servers, API clients, database calls, chat systems and crawlers. A single process can keep tens of thousands of connections open with modest memory, because each waiting connection costs a small coroutine object instead of a full thread.
| Workload | Good fit | Reason |
|---|---|---|
| Many concurrent network requests | Coroutines on an event loop | Waiting dominates, switches are cheap |
| CPU-heavy computation | Threads or processes | Needs parallel cores, not interleaving |
| UI event handling | Coroutines on the main thread | Keeps the interface responsive while awaiting |
| Blocking legacy library | Thread pool offload | Blocking calls would freeze the event loop |
| Lazy sequences and pipelines | Generator coroutines | Produce values on demand |
Modern libraries add structured concurrency, in which child coroutines live inside a scope that waits for them and cancels them together. Kotlin coroutine scopes, Python task groups and Swift task groups all follow this idea, which prevents orphaned tasks and makes error propagation predictable.
Pitfalls and common mistakes
Most coroutine bugs come from forgetting that the scheduler cannot interrupt a running coroutine. The table below lists frequent symptoms and their causes.
| Symptom | Cause | Fix |
|---|---|---|
| Whole service stalls | Blocking call inside a coroutine | Use async libraries or offload to a thread |
| Task silently never runs | Coroutine created but not awaited | Await it or schedule it as a task |
| Lost exceptions | Fire-and-forget tasks without a parent | Use structured concurrency scopes |
| Inconsistent shared state | Mutation across an await point | Use locks or keep state local |
| Deadlock on resume | Waiting synchronously for async work on the same loop | Await instead of blocking |
Another misconception is that code without threads is automatically free of races. Between two await points another coroutine can change shared data, so a check followed by an await and then an update is still a check-then-act race.
The idea in brief
A coroutine is a task that can pause itself and continue later. Think of a cook with several pots on the stove. While one pot heats up, the cook chops vegetables for another dish. Nothing is lost when the cook switches. Each dish continues exactly where it stopped.
One cook can handle many dishes this way because most of the time is spent waiting. Pausing and switching is quick and cheap. It only fails if the cook gets stuck on one long task and never looks at the other pots.
A practical scenario
A price comparison service must query 300 supplier APIs for each search, and each API takes between 100 and 800 milliseconds to answer. Calling them one after another would take minutes, and starting 300 operating system threads per search would waste memory under load.
The developers write the fetcher as async coroutines on an event loop, start all 300 requests inside a task group with a two-second timeout, and limit simultaneous connections per supplier with a semaphore. A search now completes in roughly the time of the slowest single response, and one server process handles hundreds of searches at once with a few hundred megabytes of memory.