/journal

Scanning Is Mostly Waiting

The one insight that turned a slow scanner into a fast one.

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."