Cloud Parts

Synchronous and asynchronous execution

Whether a call makes its caller wait for the result (synchronous) or hands control back at once and delivers the result later (asynchronous), and the ways languages let code wait without holding a thread.

How it works

A synchronous call finishes before the caller’s next line runs. If it reads from a socket and no data has arrived, the thread sleeps in the kernel until it does: the call blocks. A non-blocking call returns at once, with the data or with “not ready yet”, and something has to tell the program later that the data is there. Asynchronous code is built on that: the caller starts an operation, gets control back, and the result arrives later through a callback, a future, a resumed await, a channel or a message. What it buys is waiting without holding a thread. By Little’s law, a service with 50 ms of latency serving 200 requests per second has 10 requests in flight; if each holds an operating-system thread for its whole life, the thread count, not the CPU, becomes the limit, because OS threads are costly. What it costs: in the asynchronous style each request hops between threads, so stack traces lose their context and debuggers can’t step through a request, and with async/await functions split into two “colours”, because an async function returns a promise or task that only another async function can await. There are two families of answers. One makes waiting explicit in the code: callbacks, futures and promises, async/await. The other makes threads so cheap that blocking is fine again: goroutines, Java virtual threads and Erlang processes, which pass results over channels or into mailboxes. None of this is parallelism. Concurrency is dealing with many things at once; parallelism is doing many things at once. A Node.js process handles many connections concurrently on one thread, but it runs one callback at a time, and CPU-bound work gets no faster from being asynchronous.

Think of it as

Ordering coffee. Synchronous: you stand at the counter until your drink is made, and the queue behind you waits too. Asynchronous: you’re handed a buzzer (a future), you sit down, the barista takes the next order, and the buzzer goes off when your drink is ready. Parallelism is hiring a second barista.

Reach for it when

  • Work that mostly waits on the network, disks or other services: API gateways, proxies, crawlers, chat servers
  • Starting several independent calls and waiting for all of them (Promise.all, Task.WhenAll, asyncio.gather, a goroutine each)
  • Keeping a UI thread responsive while data downloads
  • Thousands of concurrent connections, where a platform thread per connection would run out of memory or threads

Think twice when

  • CPU-bound work such as image processing or number crunching; asynchrony adds no cores, so use threads or processes for parallelism
  • Small scripts and low concurrency, where plain blocking code is easier to read and debug
  • Calling blocking libraries from async code on an event loop: one blocking call stalls every task on that thread
  • Wrapping everything in futures in Go, Erlang or Java 21+, where cheap threads already let blocking code scale

A real example

A product page needs the user, their orders and recommendations from three services, each taking about 100 ms. Called one after another, the page takes 300 ms and holds a thread the whole time. Started together and awaited with Promise.all (or three goroutines), it takes about 100 ms: as long as the slowest call. With a pool of 200 platform threads that each block for 300 ms, the server tops out near 667 requests per second (200 ÷ 0.3 s) however idle its CPUs are. Written asynchronously, or run on virtual threads, a waiting request costs a small object on the heap instead of a thread.

More on synchronous and asynchronous execution

Narrower ideas that build on this one.

Blocking, non-blocking, asynchronous, parallel#

These four words describe different things. Blocking and non-blocking describe one call: does the thread stop until the operation completes, or does the call return at once? Synchronous and asynchronous describe how the result reaches the caller: as the return value, or later, through a callback, a future, an await, a channel or a message. Parallel describes the hardware: two pieces of work running at the same instant on different cores.

Microsoft’s C# guide uses breakfast to separate them. One cook can make eggs, bacon and toast asynchronously: start the eggs, and while they cook, put the bread in the toaster. Nothing runs in parallel, because there is one cook, yet breakfast is ready far sooner than if the cook stared at each pan until it finished. Parallel cooking needs a second cook. Rob Pike puts the same split in one line: concurrency is about dealing with lots of things at once, parallelism is about doing lots of things at once.

That is why asynchrony helps I/O-bound services and not CPU-bound ones. JEP 444 makes the point about Java’s virtual threads: they are not faster threads and run code no faster than platform threads; they exist for scale, so many more tasks can wait at once. If each task sorts a huge array instead of waiting, more threads than cores doesn’t help.

The six models side by side#

Each model is a different answer to “what happens to the caller while it waits?”

ModelHow the caller waitsHolds an OS thread while waiting?Coloured functions?Where you meet it
CallbacksIt doesn’t: it passes a function that the runtime calls later with the resultNoYes: anything that needs the result must itself take a callbackNode.js error-first callbacks, browser event handlers
Futures and promisesGets an object that will hold the value; chains steps onto it or blocks on get()No when chained; yes if it blocks on get()Yes: the result type is Promise<T>, Task<T>, Future<T>JavaScript Promise, Java CompletableFuture, C# Task
async/awaitWrites await; the function is suspended and its thread is freedNoYes: only an async function can awaitC#, JavaScript, Python, Rust
Green threadsMakes an ordinary blocking call; the runtime parks the lightweight threadNo: the runtime frees the OS thread underneathNo: every function is ordinaryGo goroutines, Java 21 virtual threads, Erlang processes
CSP channelsBlocks on a receive from a channel until a sender deliversOnly if the channel is used from a real OS threadNo in Go; in Clojure, channel operations park only inside go blocksGo channels, Clojure core.async, occam
Actor messagesDoesn’t wait: sends a message and later handles a reply message from its mailboxNoNo, but results come back as messages, not return valuesErlang processes, Akka actors
  • “Coloured functions” is Bob Nystrom’s name for the split between functions that return a value and functions that return a wrapper for one; he names Go, Lua and Ruby as languages without it, because they can switch between independent call stacks.

The same two calls in Go#

The snippet above does it with promises. Go needs no async keyword: fetchUser and fetchOrders are ordinary blocking functions. Each runs in its own goroutine and sends its result down a channel; receiving from the channel blocks only the receiving goroutine, so both waits overlap.

go
package main

import (
	"fmt"
	"time"
)

type User struct{ Name string }
type Order struct{ Total int }

// Plain blocking functions: no async keyword, no promise type.
func fetchUser(id int) User {
	time.Sleep(100 * time.Millisecond) // stands in for a 100 ms network call
	return User{Name: "Ada"}
}

func fetchOrders(id int) []Order {
	time.Sleep(100 * time.Millisecond)
	return []Order{{Total: 42}}
}

func main() {
	start := time.Now()
	users := make(chan User)
	orders := make(chan []Order)

	go func() { users <- fetchUser(7) }() // each call runs in its own goroutine
	go func() { orders <- fetchOrders(7) }()

	u, o := <-users, <-orders // receiving blocks this goroutine, not an OS thread
	fmt.Println(u.Name, len(o), time.Since(start).Round(10*time.Millisecond))
	// Ada 1 100ms
}

What asynchrony costs#

Colour. Bob Nystrom’s 2015 essay describes the split: synchronous functions return values, async ones return a Task<T> or Future<T> wrapper, and you can only unwrap it by making your own function async too. async/await makes red functions much easier to call but leaves the two colours, and the colour spreads up through every caller and into higher-order functions such as map.

Lost context. JEP 444 lists what Java developers gave up by moving to the asynchronous style: each stage of a request may run on a different thread, so stack traces provide no usable context, debuggers can’t step through request-handling logic, and profilers can’t tie an operation’s cost to its caller. Virtual threads keep a real stack per request, so debuggers and thread dumps work as they do for platform threads.

Blocking the loop. On a single-threaded event loop, one callback that computes for a long time, or calls a blocking API, stalls every other task. See the event-loop concept for how Node.js and Python’s asyncio handle this.

In code · js

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function fetchUser(id) {
  await sleep(100); // stands in for a 100 ms network call
  return { id, name: "Ada" };
}
async function fetchOrders(id) {
  await sleep(100);
  return [{ id: 1, total: 42 }];
}

async function main() {
  let t = Date.now();
  const user = await fetchUser(7);      // wait 100 ms...
  const orders = await fetchOrders(7);  // ...then another 100 ms
  console.log("one after the other:", Date.now() - t, "ms"); // about 200 ms

  t = Date.now();
  const [u, o] = await Promise.all([fetchUser(7), fetchOrders(7)]); // both start now
  console.log("together:", Date.now() - t, "ms"); // about 100 ms
  console.log(u.name, o.length); // Ada 1
}

main();

Often confused with

Where it came from1973–2023

The building blocks are older than most languages that use them. In 1973 Carl Hewitt and colleagues described a model in which every computation is actors sending messages. In 1976 Daniel Friedman and David Wise proposed the “promise”, and in 1977 Henry Baker and Carl Hewitt described call-by-future, where each argument is evaluated by its own process. In 1978 Tony Hoare’s “Communicating Sequential Processes” made sending and receiving primitive operations. Barbara Liskov and Liuba Shrira’s 1988 promises applied the idea to asynchronous calls between machines.

Mainstream servers meanwhile ran a thread per request, and OS threads proved too costly to have one per waiting connection. The way out was to stop holding a thread while waiting: event loops and callbacks, then futures (Java 5’s Future; JavaScript promises in ES2015) and finally async/await, which grew out of F#’s asynchronous workflows into C# 5.0 in 2012, Python 3.5 in 2015, ECMAScript 2017 and Rust 1.39 in 2019. In 2015 Bob Nystrom’s essay “What Color is Your Function?” named the price: every async function divides a codebase in two.

Another line of languages made threads cheap instead. Erlang, begun at Ericsson in 1986, runs every task as a tiny process with a mailbox. Go, open-sourced by Google in November 2009, put Hoare’s ideas into a procedural language with goroutines and channels, and Clojure’s core.async brought channels to the JVM and JavaScript in 2013. Java 21, released in September 2023, added virtual threads explicitly to give thread-per-request code the scalability of the asynchronous style without rewriting it.

Why it’s built this way

Make the wait visible in the type: Promise<T>, Task<T>, Future<T>
On an event loop the call stack is discarded while the OS does the I/O, so the program has to keep its place somewhere else. A future object, and later a compiler-generated state machine behind await, keeps that state on the heap instead of in closures written by hand.
Make threads cheap instead of changing the code
The asynchronous style scaled but lost stack traces, debuggers and profilers, and split every API in two. Goroutines and virtual threads keep blocking code and real stacks, and let the runtime free the OS thread underneath.
Communicate over channels rather than shared memory
Go’s designers found that low-level primitives such as mutexes and condition variables made concurrency hard, and that Hoare’s CSP fitted a procedural language well. Passing a value over a channel also passes its ownership, so data races can’t occur by design.
One mailbox per actor, one message at a time
If only messages can change an actor’s state and they’re processed one at a time, the actor’s invariants hold without locks, and senders never block.

Timeline

  1. 1973

    Hewitt, Bishop and Steiger describe the actor model

  2. 1977

    Baker and Hewitt describe call-by-future

  3. 1978

    Hoare publishes “Communicating Sequential Processes”

  4. 1988

    Liskov and Shrira’s promises for asynchronous remote calls

  5. 2009

    Google open-sources Go, with goroutines and channels

  6. 2012

    C# 5.0 ships async and await

  7. 2017

    ECMAScript 2017 adds async functions to JavaScript

  8. 2023

    Java 21 ships virtual threads

Sources27

Improve this page on GitHub