Why concurrency matters for I/O-bound work
The problem, before any new syntax
Picture three independent tasks an agent might need to do: call a search
API, call a weather API, call a calculator API. Each one mostly just
waits — for a network response, not for the CPU to finish computing
something. Simulate that waiting with time.sleep(), and run all three
one after another, the only way you know how to so far:
Callout:
time.sleep()runs directly in this course’s in-browser sandbox, but its blocking behavior isn’t fully reliable across every environment the way it is on a real machine — the timing shown above is what you’d see running this code locally. Starting in the next concept, this lesson switches toasyncio.sleep()for anything timing-sensitive, which is built specifically to behave correctly and consistently in an async context (including in-browser) — one more reason it’s the right tool onceasync/awaitare actually in play, not justtime.sleep()with extra syntax around it.
time.perf_counter() returns a precise timestamp — calling it before and
after, and subtracting, is a standard way to measure how long code
actually took to run. The total here is almost exactly
2 + 1 + 0.5 = 3.5 seconds — each call fully finishes (including its
wait) before the next one even starts.
Why this is wasteful
Look at what’s actually happening during call_search_api()’s two-second
time.sleep(2): your program is doing nothing. It isn’t computing
anything — it’s just waiting for a timer (standing in for a network
response) to finish, with the CPU sitting completely idle the entire
time. Meanwhile, call_weather_api() and call_calculator_api() are
just sitting in line, not even started yet, even though neither one
depends on the search result at all.
This is called I/O-bound work — time spent waiting on something external (network, disk, another process) rather than on the CPU actually computing. The three calls above have no dependency on each other’s results — there’s no real reason the weather call couldn’t be starting its own wait while the search call is still waiting on its own. Nothing about regular, sequential Python function calls lets you express that, though — one call has to fully return before the next line even runs.
What 'concurrency' means here
The rest of this lesson is about a specific tool for exactly this situation: running multiple independent, waiting operations at overlapping times, so the total time approaches the longest single wait, rather than the sum of all of them — 2 seconds instead of 3.5, for the three calls above, if all three could genuinely overlap.
This is a different kind of speedup than writing more efficient code —
each individual call still takes exactly as long as it did before
(you’re not making the network faster). What changes is how much of that
waiting time happens at once, across multiple operations, instead of one
after another with nothing else happening in between. That’s the specific
problem async/await — covered starting in the next section — is
designed to solve.
In the sequential version above, why does the total elapsed time come out to roughly 2 + 1 + 0.5 seconds, rather than something shorter?