Skip to main content

Synchronous vs Asynchronous Operations: A Developer's Guide

· 4 min read
PSVNL Sai Kumar
Senior Software Development Engineer, Oracle

Every program is built around a model of time: operations that run in order, one after another, or operations that run independently and complete whenever they're ready. Understanding synchronous vs. asynchronous execution is fundamental to writing code that performs well under load.

What Does Synchronous Mean?

Synchronous (sync) operations execute sequentially — each operation must finish before the next one begins. The program's execution flow matches the order of instructions in the code.

# Synchronous — each line waits for the previous
result1 = fetch_from_db("query_1") # blocks until done
result2 = fetch_from_db("query_2") # blocks until done
process(result1, result2)

This is easy to read and reason about. But in I/O-heavy applications, it means the CPU sits idle while waiting for disk reads, network responses, or database queries.

What Does Asynchronous Mean?

Asynchronous (async) operations do not wait for completion before moving on. Instead, they register a callback, future, or promise, and the program continues executing other code. When the result arrives, the program is notified.

# Asynchronous — both queries run in parallel
result1, result2 = await asyncio.gather(
fetch_from_db("query_1"),
fetch_from_db("query_2"),
)
process(result1, result2)

Here, both database queries are launched simultaneously. Total time ≈ the slower of the two, not their sum.

Synchronous vs. Asynchronous: Head-to-Head

SynchronousAsynchronous
Execution orderSequentialOut-of-order (callbacks/events)
Thread useHolds thread while waitingThread is free while waiting
ThroughputLimited by I/O wait timeHigh — latency is hidden
ComplexitySimple, predictableMore complex (error handling, state management)
Best forSimple scripts, CPU-bound tasks, low concurrencyWeb servers, APIs, real-time systems, high concurrency

What is Blocking I/O?

When a program makes a blocking I/O call (reading a file, opening a TCP connection, querying a database), the OS suspends the current thread until the operation completes. During this time:

  • The thread cannot do other work
  • On a server handling many requests, you need one thread per pending I/O — this limits scalability
// Blocking I/O in Java — thread is suspended at readLine()
BufferedReader reader = new BufferedReader(new FileReader("data.txt"));
String line = reader.readLine(); // thread blocks here

Non-Blocking I/O vs. Async

These terms are related but distinct:

  • Non-blocking I/O: The OS call returns immediately, even if data isn't ready yet. The program must poll or use a selector (epoll, select) to check for readiness.
  • Asynchronous I/O: The OS notifies the program when the operation is complete (via callback or signal). The program does not poll.

Most high-level async frameworks (Node.js, asyncio, goroutines) abstract non-blocking I/O into a friendlier async/await programming model.

Real-World Impact

Synchronous web server (Python, no async):

  • Each request needs a thread (or process) to handle it
  • 1000 concurrent requests = 1000 threads = high memory usage

Asynchronous web server (Node.js, FastAPI, Go):

  • A few threads serve many requests
  • I/O waits are interleaved across requests
  • 10,000 concurrent connections on a single thread is achievable

When to Use Each

Use synchronous code when:

  • You are writing scripts, batch jobs, or one-off tools
  • The bottleneck is CPU work, not I/O
  • Code clarity is more important than throughput
  • The concurrency level is low (< a few dozen parallel operations)

Use asynchronous code when:

  • You are building a web API, microservice, or real-time app
  • Requests spend most of their time waiting on I/O
  • You need to handle hundreds to thousands of concurrent connections
  • You want to hide latency by parallelizing independent I/O operations

Common Mistakes

  • Mixing blocking calls into async code: A single blocking call in an event loop blocks all pending coroutines. Use thread pools for CPU or legacy blocking code.
  • Forgetting to await: In Python and JavaScript, forgetting await returns a coroutine object, not a result. The operation never runs.
  • Over-engineering with async: Small scripts that run once and handle one request at a time don't need async. Synchronous is simpler and correct.