Some context on who is writing this and why. We are Geonode and we sell proxies, which puts us next to this topic constantly — web scraping is the canonical I/O-bound workload, and "my scraper is slow" is one of the most common things people bring to us. So here is the honest version before the theory: if your scraper is slow because it fetches one page at a time, buying proxies will not speed it up. Concurrency is a property of your code. A hundred proxy endpoints and a sequential loop gives you a sequential scraper with a hundred idle endpoints. Fix the concurrency first. Proxies solve a different problem — the one you hit after concurrency works, when the target starts rate-limiting the address that is suddenly making fifty requests a second. Both problems are real. They are not the same problem and they do not have the same solution.
With that said, the distinction.
The Distinction in One Sentence
Rob Pike's formulation from his talk on the subject remains the clearest, and the Go blog states it directly:
Concurrency is "the composition of independently executing processes."
Parallelism is "the simultaneous execution of (possibly related) computations."
Read those twice, because the difference is where the emphasis falls. Concurrency is about structure — how you decompose a problem into pieces that can proceed independently. Parallelism is about execution — how many of those pieces physically run at the same instant.
The consequence people miss: concurrency is something you write, parallelism is something the machine does. You can write a concurrent program and run it on a single core, where nothing is ever simultaneous, and it will still be concurrent. You have described independent tasks; the runtime interleaves them. And a concurrent program run on eight cores may become parallel, if the runtime and the workload permit it.
That asymmetry is why the two words are not interchangeable. Concurrency enables parallelism without guaranteeing it. Parallelism without concurrent structure is not available at all.
Why the Confusion Persists
Three reasons, and naming them helps.
The observable behaviour is often identical. A concurrent program on one core and a parallel program on four both look like "several things happening". From outside, you cannot tell which you have. You find out when you add cores and nothing improves.
The vocabulary of every language is inconsistent. Python's threading module gives you concurrency but historically not parallelism. Python's multiprocessing gives you both. JavaScript's async/await gives you concurrency and never parallelism for your own code. Go's goroutines give you concurrency and parallelism up to GOMAXPROCS. The same words in the documentation mean different things across ecosystems.
Most of the time you do not need to care, until suddenly you do. For an I/O-bound workload the distinction is nearly academic — concurrency alone gets you the whole win. For a CPU-bound workload it is the entire ballgame, because concurrency alone gets you nothing. The trouble is that people learn the pattern that worked on their I/O-bound problem and apply it to a CPU-bound one.
How Each Language Actually Does It
| Runtime | Concurrency mechanism | Real parallelism for your code | Where it breaks down |
|---|
| Python (default build) | threading, asyncio | Only via multiprocessing | GIL serialises bytecode execution |
| Python (free-threaded build) | threading, asyncio | Yes, threads run in parallel | Single-threaded overhead, ecosystem maturity |
| Go | goroutines + channels | Yes, up to GOMAXPROCS | Shared state still needs synchronisation |
| Node.js | event loop, async/await | Only via worker_threads or child processes | One CPU-bound callback blocks everything |
| Java / C# | threads, thread pools | Yes | Complexity of shared mutable state |
| Rust | async + threads | Yes | Compiler forces you to be correct first |
Node.js is the cleanest illustration of concurrency without parallelism. The official documentation puts it precisely: the event loop "allows Node.js to perform non-blocking I/O operations — despite the fact that a single JavaScript thread is used by default — by offloading operations to the system kernel whenever possible." The kernel is multi-threaded; your JavaScript is not. The loop cycles through six phases — timers, pending callbacks, idle/prepare, poll, check, close callbacks — and when an operation finishes, "the kernel tells Node.js so that the appropriate callback may be added to the poll queue to eventually be executed."
The practical consequence follows directly. Ten thousand concurrent HTTP requests in Node is trivial, because the waiting happens in the kernel. One CPU-bound function that runs for two seconds freezes the entire process, because there is only one thread to run it on and nothing can preempt it.
Go takes the opposite approach: goroutines are cheap enough to create by the thousand, and the scheduler distributes them across OS threads up to GOMAXPROCS, which defaults to the number of available cores. So Go gives you concurrency and parallelism from the same construct. This is why Pike gave the talk — the distinction matters most in a language where you get both and can therefore confuse them.
The Deciding Question: Is Your Work I/O-Bound or CPU-Bound
Everything above reduces to one question about your workload, and it is worth measuring rather than assuming.
I/O-bound means your program spends most of its time waiting: for a network response, a disk read, a database query. During the wait, the CPU is idle. Concurrency is the correct and sufficient answer, because concurrency lets you start the next wait while the current one is still pending. Parallelism adds essentially nothing — eight cores waiting for the network are not faster than one core waiting for the network.
CPU-bound means your program spends most of its time computing: parsing, compressing, hashing, transforming. The CPU is saturated. Concurrency alone changes nothing — interleaving two computations on one core takes the same total time as running them in sequence, plus switching overhead. Only parallelism helps, and only up to the number of physical cores.
To find out which you have, measure rather than reason. On Linux, time gives you the answer immediately: compare real elapsed time against user plus system CPU time. If real time is far greater than CPU time, you are waiting — I/O-bound. If they are close, you are computing — CPU-bound.
Web scraping is a useful example because it is both, in sequence. Fetching pages is heavily I/O-bound; parsing the HTML afterwards is CPU-bound. The right architecture uses concurrency for the fetching and parallelism for the parsing, and the common mistake is applying one strategy to both stages. A scraper with 200 concurrent fetches feeding a single-threaded parser is not a fast scraper — it is a fast fetcher with a queue backing up behind it.
Python's GIL and What Free Threading Changed
Python deserves its own section because its situation has genuinely changed and much of what you will read about it is now out of date.
Historically: CPython's Global Interpreter Lock permitted only one thread to execute Python bytecode at a time. Threads therefore gave concurrency but not parallelism. The GIL is released around I/O operations, so threaded I/O worked fine; CPU-bound threading did not, and multiprocessing was the workaround.
What changed: starting with the 3.13 release, CPython ships an optional build with the GIL disabled. The free-threading documentation describes it plainly — "Free-threaded execution allows for full utilization of the available processing power by running threads in parallel on available CPU cores."
PEP 779, accepted by the Steering Council on 16 June 2025 with Final status, set the criteria for moving free threading from experimental to officially supported, targeting Python 3.14 for that phase.
Four practical points before you reach for it:
It is not the default build. You have to obtain or build it deliberately — from source, that means the --disable-gil configure option. Check what you are running with python -VV, which shows "free-threading build", or sys._is_gil_enabled(), which returns False when the GIL is off.
Single-threaded code gets slower. The documentation reports that on the pyperformance suite "the average overhead ranges from about 1% on macOS aarch64 to 8% on x86-64 Linux systems". PEP 779 records the Steering Council expecting free-threaded Python "to be around 10-15% slower", with 15% as the hard target for phase II, and accepts a 20% geometric mean increase in memory use as "the cost of having efficient, safe free-threading". If your program is single-threaded, this build is a straight regression.
You can put the GIL back at runtime. Free-threaded builds support running with the GIL enabled via the PYTHON_GIL environment variable or the -X gil option — useful when a dependency misbehaves.
Your dependencies are the constraint. C extensions must be built to declare free-threading support. The ecosystem has moved substantially, but "it works on my machine with pure Python" is not the same as "my scientific stack works".
For the I/O-bound case that dominates scraping and API work, none of this changes your decision: asyncio or a thread pool on the standard build already gives you everything concurrency can give. Free threading matters when the parsing stage, not the fetching stage, is your bottleneck.
A Worked Example: Fetching 10,000 URLs
Concrete numbers make the distinction obvious. Assume each request takes 200 ms and parsing each response takes 50 ms of CPU, on a four-core machine.
Sequential. 10,000 × 250 ms = 2,500 seconds, about 42 minutes. The CPU is idle 80% of that time.
Concurrent fetching, sequential parsing. With 100 concurrent requests, fetching drops to roughly 20 seconds of wall clock. Parsing is untouched: 10,000 × 50 ms = 500 seconds. Total about 520 seconds, roughly 9 minutes. A 4.8× improvement — and notice where the time went. Fetching was 80% of the original runtime and is now 4% of the new one. Parsing, which you did not change, is now 96% of everything.
Concurrent fetching, parallel parsing on four cores. Parsing drops to about 125 seconds. Total about 145 seconds, roughly 2.5 minutes. A 17× improvement over sequential.
Three lessons live in those numbers.
First, the biggest win comes from fixing the I/O with concurrency, and it is nearly free — no extra cores, no shared-state problems, just a different loop.
Second, once you fix the dominant bottleneck, the next one immediately dominates. This is Amdahl's law in its most practical form: optimising a stage that is 20% of runtime cannot make you more than 25% faster no matter how completely you eliminate it. Always measure before optimising, and measure again afterwards, because the answer changes.
Third — and this is where our commercial interest becomes relevant, so weigh it accordingly — the moment you go from one request at a time to a hundred, you become visible. A single address making 500 requests per second to one host will be rate-limited, then blocked. That is not a concurrency problem and no amount of asyncio fixes it; it is a distribution problem, and it is what proxies are for. Our residential traffic starts at $0.79/GB and datacentre at $0.14/GB, checked against our pricing page in September 2026. But note the ordering: concurrency first, then proxies when concurrency creates a problem it cannot solve. Doing it the other way round means paying for bandwidth you have no way to use.
Where Concurrency Stops Helping
Adding more concurrency has diminishing and then negative returns, and the turning point arrives sooner than most people expect.
Connection limits. Operating systems cap open file descriptors. Servers cap concurrent connections per client. Ten thousand concurrent requests from one machine will hit one of these ceilings well before it hits a CPU limit, and the failure mode is usually a confusing error rather than a clear one.
Memory. Every in-flight request holds buffers, parsed headers and pending response data. Ten thousand concurrent requests each holding 100 KB is a gigabyte of memory doing nothing but waiting.
Context-switching overhead. OS threads are not free — each carries a stack and a scheduling cost. This is exactly why goroutines and coroutines exist: they are cheap enough that thousands are reasonable, where thousands of OS threads are not.
The target's tolerance. The other end of the connection has opinions. Past some rate, additional concurrency produces 429s and 503s rather than data, and your effective throughput falls as you add more. This is the most common real-world ceiling and the one least often measured, because the requests still "work" — they just return errors that a retry loop dutifully repeats.
The practical approach is unglamorous: start with a modest concurrency limit, measure completed-requests-per-second rather than requests-attempted, and increase until throughput stops improving. It will plateau, then decline. The optimum is at the plateau, and it is usually a much smaller number than intuition suggests — often tens rather than hundreds.
When You Need Neither
Worth stating, because "make it concurrent" has become a reflex.
When the work is genuinely small. A hundred requests taking 200 ms each is 20 seconds sequentially. If it runs nightly in a cron job, 20 seconds is fine and concurrent code is harder to debug when it fails at 3 a.m.
When ordering is part of the requirement. Some pipelines must process items in strict order, or each step depends on the previous result. Concurrency here is not merely unhelpful; it is a source of bugs that appear only under load.
When the bottleneck is somewhere else entirely. If your database writes are the constraint, 200 concurrent readers just build a longer queue in front of the same lock. Fix the actual bottleneck. Concurrency upstream of a serial resource converts a slow program into a slow program with a memory problem.
When the shared state is complicated. Concurrent code that touches shared mutable state needs synchronisation, and getting it wrong produces the worst class of bug — intermittent, load-dependent, unreproducible on your machine. If the speedup is 2× and the state is intricate, sequential code you can reason about is frequently the better engineering decision.
And the version relevant to us: if you are scraping a few hundred pages a day from a site that does not care, you need neither concurrency nor proxies. One requests call in a loop with a polite delay is the right answer, and we would rather tell you that than sell you a plan you do not need.
People Also Ask
What is the simplest difference between concurrency and parallelism?
Concurrency is dealing with many things at once — a structural property of how you write the program. Parallelism is doing many things at once — a physical property of how it executes. Concurrency makes parallelism possible; it does not make it happen.
Can you have parallelism without concurrency?
Not usefully in the sense discussed here. Parallel execution requires independent units of work to distribute, and defining those units is what concurrency means. Hardware-level parallelism such as SIMD is an exception — it parallelises a single instruction stream over data without any concurrent structure in your program.
Does the Python GIL still exist?
Yes, in the default build. Since 3.13 CPython also ships an optional free-threaded build with the GIL disabled, and PEP 779 moved that build toward officially supported status targeting 3.14. The default build still has it, so unless you deliberately installed a free-threaded interpreter, the GIL is present.
Is async the same as multithreading?
No. Async concurrency uses a single thread with cooperative switching at explicit await points, so only one piece of your code runs at a time and switches happen only where you wrote them. Multithreading uses several OS threads with preemptive switching that can occur anywhere. Async is easier to reason about; threads can achieve real parallelism where the runtime allows it.
How many concurrent requests should I make?
Fewer than you think. Start around 10, measure completed requests per second, and increase until that number stops rising. The ceiling is usually the target server's tolerance rather than your machine's capacity, and past that point extra concurrency produces errors rather than throughput.
Does concurrency make my code faster?
Only if you are waiting for something. For I/O-bound work the gains are large. For CPU-bound work on a single core, concurrency makes things marginally slower because of switching overhead — you need parallelism, which means multiple cores and a runtime that can use them.
What is the difference between multiprocessing and multithreading?
Threads share memory within one process, which makes communication cheap and shared state dangerous. Processes have separate memory, which makes them safe and communication expensive. In default-build Python, processes are how you get real parallelism for CPU-bound work; threads give you concurrency for I/O-bound work.
Do I need proxies to run concurrent requests?
Not inherently. You need them when concurrency makes you visible enough that a target rate-limits or blocks the address you are coming from. Against an API with a generous quota, or a site you have permission to crawl, concurrency alone is fine. Against a site that limits per address, distribution becomes the constraint — and that is a separate purchase from fixing your code.
Wrapping Up
The distinction is worth holding onto because it converts a vague question — "how do I make this faster?" — into a specific one with a testable answer: am I waiting, or am I computing?
If you are waiting, you need concurrency, and you need it in whatever form your language provides. The win is large, it usually costs no extra hardware, and it is available in every mainstream runtime. If you are computing, concurrency alone will do nothing for you and you need genuine parallelism — processes, worker threads, goroutines across cores, or in Python's case possibly a free-threaded interpreter with its own trade-offs to weigh.
Most real programs are both, in stages, and the ordering matters more than the choice. Fix the dominant bottleneck, measure again, and expect the answer to have moved. A pipeline that was 80% network-bound becomes 96% parse-bound the moment you fix the network, and the second optimisation is a completely different piece of work from the first.