Check If a Port Is Open
Problem statement
Check whether TCP ports answer, using only bash and timeout, against a small web server the script starts on 127.0.0.1. This is how you tell "the service is down" from "the network is blocked" when tools like nc, telnet or curl are not installed, and it is the core of every port health check.
The script picks two free ports on 127.0.0.1: it starts a test web server (python3 -m http.server) on the first and leaves the second unused.
- Wait until the web server is ready.
- Check both ports and print
openorclosed. - Stop the server and check its port again.
Expected output:
== check each port ==web port: openunused port: closed== after stopping the server ==web port: closedHints
/dev/tcp/HOST/PORT makes a real TCP connection: exec 3<>/dev/tcp/127.0.0.1/80 succeeds only if something accepts the connection.Approach
Optimal: bash /dev/tcp with timeout
Covers: TCP connect checks, bash /dev/tcp/host/port, timeout, exit codes, open vs closed vs filtered, waiting for a service to start, nc -z, $!, ${!name}.
Three possible answers. When you try to connect to a port, one of three things happens:
accepted"]:::green ~~~ C["closed
refused at once"]:::yellow ~~~ F["filtered
no answer, timeout"]:::red end classDef blue fill:#dbeafe,stroke:#2563eb,color:#1e3a8a,stroke-width:2px classDef yellow fill:#fef3c7,stroke:#d97706,color:#78350f,stroke-width:2px classDef green fill:#d1fae5,stroke:#059669,color:#064e3b,stroke-width:2px classDef red fill:#fee2e2,stroke:#dc2626,color:#7f1d1d,stroke-width:2px classDef purple fill:#ede9fe,stroke:#7c3aed,color:#4c1d95,stroke-width:2px classDef gray fill:#f3f4f6,stroke:#6b7280,color:#111827,stroke-width:2px linkStyle default stroke:#94a3b8,stroke-width:2px style R fill:transparent,stroke:#6b7280,stroke-width:2px
| Result | What happened | Means |
|---|---|---|
| open | the connection is accepted | a program is listening |
| closed | an instant "connection refused" | the host is up, but nothing listens there |
| filtered | no answer at all, until a timeout | a firewall is dropping the packets, or the host is down |
"Closed" and "filtered" lead to different fixes: start the service, or fix the firewall. That is why the timeout matters: without one, a filtered port makes your check hang for a minute or more.
Bash can connect by itself. /dev/tcp/HOST/PORT is not a real file. Bash treats a redirect to that path as "open a TCP connection". exec 3<>/dev/tcp/127.0.0.1/8080 connects and keeps the connection on descriptor 3. It succeeds (exit code 0) only if the connection was accepted. This works in bash on any Linux box, even without nc, telnet or curl.
timeout stops it hanging. timeout 1 cmd kills cmd if it is still running after one second, and returns 124. Running the check in timeout 1 bash -c "..." gives every port at most one second. The 2>/dev/null hides bash's "Connection refused" message.
Waiting for a service to start. A freshly started server needs a moment before it listens. The loop tries up to 50 times, a tenth of a second apart, and stops as soon as the port answers. Deploy scripts use the same loop to wait for an app before sending it traffic.
up to 50 times"]:::gray W --> T T --> R["ready: run the checks"]:::green classDef blue fill:#dbeafe,stroke:#2563eb,color:#1e3a8a,stroke-width:2px classDef yellow fill:#fef3c7,stroke:#d97706,color:#78350f,stroke-width:2px classDef green fill:#d1fae5,stroke:#059669,color:#064e3b,stroke-width:2px classDef red fill:#fee2e2,stroke:#dc2626,color:#7f1d1d,stroke-width:2px classDef purple fill:#ede9fe,stroke:#7c3aed,color:#4c1d95,stroke-width:2px classDef gray fill:#f3f4f6,stroke:#6b7280,color:#111827,stroke-width:2px linkStyle default stroke:#94a3b8,stroke-width:2px
Walking through the code. The # Setup: lines only start the test server on a free port, so skip past them. free_port asks Python for a port number nobody is using, so the script never clashes with real services. Because the numbers change every run, the output uses names instead.
is_openruns the/dev/tcpcheck with a one-second timeout.- The loop waits for the server, then checks
webandunused.${!name}reads the variable whose name is inname, so the same loop line checks$weband then$unused. killstops the server andwaitwaits for it to exit; the check now fails, because nothing listens any more.
Edge cases. An open port only proves something accepted the connection, not that the service works; a health check should also send a real request (next pages). Checking another machine's ports from a script counts as scanning, so only do it on systems you are allowed to test. This page needs python3 for the test server.
# Setup: start a test web server on a free port on 127.0.0.1, and pick a second free port
cd "$(mktemp -d)"
free_port() { python3 -c 'import socket; s = socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1])'; }
web=$(free_port)
python3 -m http.server "$web" --bind 127.0.0.1 > server.log 2>&1 &
server=$!
unused=$(free_port)
# Open a TCP connection with bash itself; give up after 1 second
is_open() { timeout 1 bash -c "exec 3<>/dev/tcp/$1/$2" 2>/dev/null; }
for _ in $(seq 1 50); do is_open 127.0.0.1 "$web" && break; sleep 0.1; done
echo "== check each port =="
for name in web unused; do
port=${!name}
if is_open 127.0.0.1 "$port"; then echo "$name port: open"; else echo "$name port: closed"; fi
done
kill "$server"; wait "$server" 2>/dev/null
echo "== after stopping the server =="
if is_open 127.0.0.1 "$web"; then echo "web port: open"; else echo "web port: closed"; fiInterview follow-ups
Check a list of host:port pairs and exit 1 if any is not open.
Read the list line by line, split on the colon, and keep a failure count:
while IFS=: read -r h p; do is_open "$h" "$p" && echo "OK $h:$p" || { echo "FAIL $h:$p"; fails=$((fails+1)); }; done < targets.txt, thenexit $(( fails > 0 )). With many targets, run the checks in the background with&andwait, as on the background jobs page, so a few dead hosts do not each cost a full timeout in a row.
Frequently asked questions
If it is installed, nc -zv -w 1 host 80 does the same check and prints a clear message; -z means "just test, send nothing" and -w 1 is the timeout. The catch is that there are several different nc versions with different flags, and minimal images often have none. /dev/tcp works wherever bash is, which makes it the safe choice inside scripts and containers. Note that /dev/tcp is a bash feature: it does not work in sh or dash.
The program is running, so look at the path in between. Check what address it binds to with ss -tlnp: 127.0.0.1 means local only. If it binds to 0.0.0.0, check the host firewall and, in the cloud, the security group or network ACL. If the check from outside hangs until the timeout instead of failing at once, packets are being dropped, which points at a firewall rather than the app.
UDP has no handshake, so there is no clean "open" answer. A closed UDP port may send back an ICMP "port unreachable", but a firewall can drop that too, so silence means "open or filtered". The useful test is protocol-specific: dig @host example.com for DNS on 53, or ntpdate -q host for NTP. Bash's /dev/udp/host/port can send a packet, but cannot tell you whether anything received it.