Hi folks! If you have ever piped a Masscan scan into another tool, you know the feeling: the scan itself takes seconds, and then you spend twenty minutes figuring out why your parser chokes on the output. Masscan has five output formats, a short flag and a long flag for each of them, and a handful of small behaviours that are not written down anywhere obvious.
So we sat down with a real Masscan 1.3.2 build, scanned one of our own servers, and captured what every format actually prints — byte for byte. No copy-pasting from the man page, no guessing. Everything you see below came out of a terminal.
Grab a coffee, open a shell, and let’s go through all of it: JSON, list, XML, grepable, binary, writing to stdout, and the three gotchas that will bite you when you automate this.
Table of Contents
- All Five Formats at a Glance
- Writing to stdout: the
-Trick - -oJ: JSON Output, Field by Field
- -oL: The List Format
- -oX: XML (Nmap-Compatible-ish)
- -oG: Grepable Output
- -oB and –readscan: Scan Once, Convert Later
- The Long Form: –output-format and –output-filename
- Three Gotchas That Break Automation
- Practical Pipelines
- Conclusion
All Five Formats at a Glance
Masscan writes results in five formats. Each has a short flag, and each short flag is just a shortcut for a pair of longer options that we will cover further down.
| Flag | Format | Best for |
|---|---|---|
-oJ | JSON | Automation, jq, feeding databases |
-oL | List (plain text) | Quick awk/cut work, human reading |
-oX | XML | Tools that already ingest Nmap XML |
-oG | Grepable | One line per host, classic grep pipelines |
-oB | Binary | Fastest writes; convert to any format later |
Every example below scans the same target — a server we own — on ports 80 and 443, at a deliberately low rate. Please only point these commands at hosts you are allowed to scan.
Writing to stdout: the - Trick
This is the single most searched question about Masscan output, so let’s answer it first: yes, a single dash sends results to stdout, and it works with every format flag.
masscan 203.0.113.10 -p80,443 --rate 100 -oJ -
The important detail: the progress meter that Masscan prints while running goes to stderr, not stdout. That means results and progress never get mixed up, and you can pipe cleanly as long as you silence stderr:
masscan 203.0.113.10 -p80,443 --rate 100 -oJ - 2>/dev/null | jq .
Without 2>/dev/null your terminal fills with rate: 0.00-kpps, 100.00% done lines, but your pipe still receives valid data. If a downstream tool ever complains, stderr is almost always the culprit.
-oJ: JSON Output, Field by Field
Here is the literal output of a two-port scan, exactly as Masscan 1.3.2 printed it:
[
{ "ip": "203.0.113.10", "timestamp": "1786719570", "ports": [ {"port": 443, "proto": "tcp", "status": "open", "reason": "syn-ack", "ttl": 58} ] }
,
{ "ip": "203.0.113.10", "timestamp": "1786719570", "ports": [ {"port": 80, "proto": "tcp", "status": "open", "reason": "syn-ack", "ttl": 58} ] }
]
A few things worth noticing, because they trip people up:
- It is one JSON array, not newline-delimited JSON. The commas sit on their own lines, which looks odd but parses fine.
timestampis a string, not a number — note the quotes. If your schema expects an integer, cast it.portsis always an array, even though a single result record only ever contains one port. Index it as.ports[0].- One record per port, not per host. Two open ports on one IP produce two objects, each repeating the same IP.
- There is no service name. Unlike the grepable format, JSON gives you the port number and nothing else about what runs there.
The output is valid enough for jq to consume directly, which is what most people actually want:
masscan 203.0.113.10 -p80,443 --rate 100 -oJ - 2>/dev/null
| jq -r '.[] | "(.ip) (.ports[0].port) (.ports[0].status)"'
203.0.113.10 80 open
203.0.113.10 443 open
-oL: The List Format
The list format is the one to reach for when you just want columns to hand to awk:
#masscan
open tcp 443 203.0.113.10 1786719582
open tcp 80 203.0.113.10 1786719582
# end
The columns are: status, protocol, port, IP, timestamp. Note that the order puts the port before the address, which is the opposite of what most people assume when they write their first awk '{print $4}'. Both the header and the footer start with #, so a single grep -v '^#' gives you clean data.
-oX: XML (Nmap-Compatible-ish)
XML output mimics Nmap’s structure closely enough that many Nmap-aware tools will read it:
<?xml version="1.0"?>
<!-- masscan v1.0 scan -->
<nmaprun scanner="masscan" start="1786719610" version="1.0-BETA" xmloutputversion="1.03">
<scaninfo type="syn" protocol="tcp" />
<host endtime="1786719610"><address addr="203.0.113.10" addrtype="ipv4"/><ports><port protocol="tcp" portid="443"><state state="open" reason="syn-ack" reason_ttl="58"/></port></ports></host>
<runstats>
<finished time="1786719621" timestr="2026-08-14 15:00:21" elapsed="11" />
<hosts up="1" down="0" total="1" />
</runstats>
</nmaprun>
The root element is nmaprun with scanner="masscan", and you get a runstats block with elapsed time and host counts — handy if you want to log how long a sweep took without timing it yourself.
-oG: Grepable Output
Grepable output is the only format that tells you the service name associated with the port:
# Masscan 1.3.2 scan initiated Fri Aug 14 15:00:21 2026
# Ports scanned: TCP(1;443-443) UDP(0;) SCTP(0;) PROTOCOLS(0;)
Timestamp: 1786719621 Host: 203.0.113.10 () Ports: 443/open/tcp//https//
# Masscan done at Fri Aug 14 15:00:32 2026
Look at the Ports: field — 443/open/tcp//https//. That https is a lookup from the IANA port list, not something Masscan detected on the wire. It is a label, not evidence. If you need to know what is really listening, you want banner grabbing, not a name from a table.
The header also documents exactly which port ranges were requested, which makes these files pleasantly self-describing months later.
-oB and –readscan: Scan Once, Convert Later
This is the most underused feature in the whole tool, and it is genuinely useful at scale. Write the raw results in binary, then convert them to whatever format you need — without rescanning:
# scan once, store compactly
masscan 203.0.113.10 -p443 --rate 100 -oB scan.bin
# later: turn the same file into JSON
masscan --readscan scan.bin -oJ -
# or into XML, or a list, without touching the network again
masscan --readscan scan.bin -oX report.xml
In our test a single-port result took 213 bytes on disk, and --readscan reproduced the JSON record perfectly. When you are sweeping millions of addresses, writing binary and converting afterwards saves both disk and CPU during the scan itself — and the scan is the part you do not want to slow down.
The Long Form: –output-format and –output-filename
Every short flag expands into two long options. These two commands are exactly equivalent:
masscan 203.0.113.10 -p443 --rate 100 -oJ -
masscan 203.0.113.10 -p443 --rate 100 --output-format json --output-filename -
We verified both produce byte-identical records. The long form matters in one situation: configuration files. Masscan can read its settings from a file, and there you cannot use the short flags — you write the options as key/value pairs instead:
# scan.conf
output-format = json
output-filename = results.json
rate = 1000
ports = 80,443
masscan -c scan.conf 203.0.113.10
Three Gotchas That Break Automation
1. An Empty Result Is an Empty File
Scan a closed port and ask for JSON, and you do not get []. You get nothing at all — zero bytes, no brackets. We checked this against a port we knew was closed, and the output was completely empty.
This matters because jq on an empty input does not return an empty list, it simply produces no output, and a strict parser may throw. Guard for it:
OUT=$(masscan "$TARGET" -p"$PORTS" --rate 1000 -oJ - 2>/dev/null)
if [ -z "$OUT" ]; then
echo "no open ports found"
else
echo "$OUT" | jq -r '.[] | .ip'
fi
2. Interrupting a Scan Does Not Corrupt the JSON
You will read in a lot of places that pressing Ctrl+C leaves you with a truncated, unparseable JSON file. We tested exactly that on 1.3.2: started a scan across 3000 ports, interrupted it mid-flight, and looked at the file.
It was closed properly, with the final ] in place and all three discovered ports recorded. So on current versions you can interrupt with reasonable confidence. Still worth validating in a pipeline, but it is not the landmine it is rumoured to be.
3. Banners Need More Than a Flag
Adding --banners does not automatically enrich your JSON. Masscan uses its own TCP stack, so the operating system sees the incoming replies as unsolicited and answers them with RST packets, killing the connection before a banner arrives.
On Linux you either dedicate a source IP that the kernel does not own (--source-ip) or drop the outgoing resets with a firewall rule. On Windows this is not necessary — Windows does not send those RSTs in the first place. If your --banners run comes back empty, this is almost always why.
Practical Pipelines
A few one-liners we actually use day to day.
Just the IP:port pairs, ready for the next tool:
masscan -iL targets.txt -p80,443 --rate 10000 -oJ - 2>/dev/null
| jq -r '.[] | "(.ip):(.ports[0].port)"' > live.txt
Same thing without jq, using the list format:
masscan -iL targets.txt -p80,443 --rate 10000 -oL - 2>/dev/null
| grep -v '^#' | awk '{print $4":"$3}' > live.txt
Count open ports by port number:
masscan --readscan scan.bin -oL -
| grep -v '^#' | awk '{print $3}' | sort -n | uniq -c | sort -rn
Hand results to Nmap for real service detection:
masscan -iL targets.txt -p1-65535 --rate 20000 -oJ - 2>/dev/null
| jq -r '.[] | .ip' | sort -u > hosts.txt
nmap -sV -iL hosts.txt
That last pattern is the classic division of labour: Masscan finds what is open across a huge range, Nmap tells you what it actually is on the few hosts that matter.
Conclusion
Short version, if you skipped to the end: use -oJ - when a machine reads the output, -oL - when you read it yourself, -oG when you want the service label, -oX when an Nmap-aware tool is next in the chain, and -oB when you are scanning at a scale where every write counts.
And remember the three quirks: an empty result is an empty file, timestamp comes back as a string, and --banners needs firewall or source-IP work on Linux before it produces anything.
If you would rather not maintain scan nodes, tune rate limits and babysit output files at all, that is precisely what we built ScaniteX for — you pick the ranges and ports, we run the scan across our own infrastructure and hand you the results ready to download. Either way, happy scanning, and keep it to networks you are allowed to touch.
Try ScaniteX for Free!
Automated platform for scanning open ports and detecting active services online.
Start a 24-hour trial period (promo code FREE10) to test all scanning features for your business security.
Get Free Trial
EN
Русский
Leave a Comment