I wrote my own port scanner, pyscan, the obvious synchronous way first: loop the ports, connect to each, write down the answers. It worked. It was also painfully slow, and understanding why is the whole lesson.
A port scan barely touches the CPU. It spends almost all its time waiting, for a port to answer, or for the connection to time out. Doing that one port at a time is like phoning a hundred people back to back and sitting through every ring in silence.
The fix is concurrency, not raw speed:
import asyncio
async def check(host, port):
try:
_, w = await asyncio.wait_for(asyncio.open_connection(host, port), timeout=1)
w.close()
return port, True
except (OSError, asyncio.TimeoutError):
return port, False
async def scan(host, ports):
return await asyncio.gather(*(check(host, p) for p in ports))
You fire off hundreds of attempts and let them all wait together. A scan that took minutes takes seconds, and the CPU never breaks a sweat. The first time it tore through a /24 in under a minute I made a noise I'm not proud of.
The principle outlived the scanner: any time the work is "wait on I/O," the answer is almost never "do it faster," it's "do more of it at once."