Reading connection states, mapping ports to processes, catching conflicts before they hit
Anyone who cannot start a service because a port is already in use, or needs to know which process is really behind a connection, cannot avoid ss. This article explains why ss replaced the old netstat, how to correctly read connection states like ESTABLISHED and TIME_WAIT, and how to reliably diagnose port conflicts before starting a service.
Table of Contents
- 1. Why Connection Analysis Belongs in Your Daily Toolkit
- 2. ss vs. netstat: Why ss Replaced the Old Tool
- 3. ss Basics: Syntax and the Most Important Options
- 4. Reading Connection States: LISTEN, ESTABLISHED, TIME_WAIT
- 5. Finding Which Process Owns a Port
- 6. Diagnosing Address-Already-in-Use Errors Before Starting a Service
- 7. Filter Expressions and Connection Statistics
- 8. netstat: When the Legacy Tool Still Makes Sense
- 9. ss and netstat Side by Side
- 10. Summary
- 11. FAQ
1. Why Connection Analysis Belongs in Your Daily Toolkit
On every Linux server running an online store, a database, or a queue consumer, TCP and UDP connections are constantly opening, closing, and moving through waiting states. A PHP-FPM pool that stops responding, a database connection that never gets closed, or a deployment that aborts with a cryptic bind error: in all of these cases the first step is the same, namely a look at the system's current network connections. Without that step, troubleshooting turns into pure guessing.
The two classic tools for this job are ss and netstat. Both show open sockets, their state, and, with the right privileges, the process behind them. The difference is not in the basic task but in the implementation: how fast the tool delivers data, how reliable the output is with thousands of simultaneous connections, and how well the result can be processed further in scripts. These exact questions decide which tool is the right choice in production.
2. ss vs. netstat: Why ss Replaced the Old Tool
netstat comes from the net-tools package, whose development has been practically stalled for years. The tool reads its information from the text files under /proc/net/tcp, /proc/net/tcp6, and /proc/net/udp, parses them line by line, and then separately resolves additional information such as the process name for every single connection. With a few dozen connections this goes unnoticed, but on a heavily loaded web server with tens of thousands of simultaneous sockets, netstat becomes noticeably slow.
ss is part of the iproute2 package and talks directly to the kernel via netlink sockets, specifically the inet_diag interface. The kernel delivers the socket information in a structured binary format instead of text, which eliminates parsing entirely and completes the query in milliseconds even with a very large number of connections. For several years now, iproute2 has shipped preinstalled on practically every mainstream distribution, while net-tools is often missing on minimal images such as slim Docker base images and has to be installed manually. That is exactly why the official documentation of many distributions now recommends ss as the standard tool.
3. ss Basics: Syntax and the Most Important Options
The invocation ss [options] [filter] follows a simple pattern. The most important options can be freely combined: -t shows TCP sockets, -u shows UDP sockets, -l restricts output to listening sockets, -a additionally shows all active connections, -n suppresses DNS resolution of addresses and noticeably speeds up the output, and -p shows the process name along with the PID, but requires root privileges or sudo to do so. The combination ss -tlnp is therefore the command typed most often in practice.
Without -n, ss tries to resolve every IP address into a hostname via reverse DNS. With a list of hundreds of connections, these DNS lookups add up to a noticeable delay, especially when the DNS server itself responds slowly. The pattern for fast, script-friendly queries is therefore to always pass -n whenever the result will be processed automatically or read under time pressure.
# List all listening TCP sockets with owning process (needs root/sudo)
sudo ss -tlnp
# Example output
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=1284,fd=6))
LISTEN 0 4096 127.0.0.1:9000 0.0.0.0:* users:(("php-fpm8.3",pid=2201,fd=9))
LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=901,fd=3))
# All TCP sockets, numeric output, no DNS lookups
ss -atn
# Only currently established connections
ss -tan state established
4. Reading Connection States: LISTEN, ESTABLISHED, TIME_WAIT
TCP connections move through a fixed state machine, and anyone who knows the most important states can correctly interpret a ss output within seconds. LISTEN means a process has bound a port and is waiting for incoming connections, with no data flowing yet. ESTABLISHED is a fully set up connection with active data exchange in both directions, the normal state for any running request. SYN-SENT and SYN-RECV mark the three-way handshake currently in progress and should generally only be visible briefly.
TIME-WAIT occurs after a connection has been actively closed and lasts two minutes by default (twice the maximum segment lifetime). This state prevents delayed packets from an old connection from being mistakenly attributed to a new one. Many short-lived TIME-WAIT connections on a web server with high request volume are completely normal and not an error. CLOSE-WAIT, on the other hand, is an important warning sign: the remote peer has already closed the connection, but the local application has not yet closed its socket. If CLOSE-WAIT entries keep piling up over time, that almost always points to a socket leak in the application, for example a missing connection close after an HTTP request in a PHP worker or a backend service.
5. Finding Which Process Owns a Port
The question "who is currently listening on port 3306?" can be answered directly with a filter expression instead of manually scanning the full list. ss supports its own filter syntax with the keywords sport and dport for source and destination port, so the output can already be narrowed down at the kernel level before anything even appears on screen. That is considerably more efficient than printing the entire list and then filtering with grep for the port you are looking for.
Without sudo, ss -tlnp still shows all listening ports, but the column with process name and PID stays empty for sockets owned by other users, since the kernel only reveals that information to the owner of the process or to root for security reasons. Good alternatives are lsof -i :PORT, which serves the same purpose from the file descriptor perspective, or fuser PORT/tcp, which directly prints the PID of the process holding the port and works well for quick ad hoc checks inside scripts.
# Which process owns port 3306 (MySQL)?
sudo ss -tlnp sport = :3306
# Alternative tools if ss is not available
sudo lsof -i :3306
sudo fuser 3306/tcp
# Check before starting a service to avoid a bind failure
sudo ss -ltn sport = :8080 | grep -q LISTEN && echo "Port 8080 is already in use" || echo "Port 8080 is free"
# Real failure from systemd when the port is already taken
sudo systemctl start nginx
# Job for nginx.service failed because the control process exited with error code.
# nginx[1421]: bind() to 0.0.0.0:80 failed (98: Address already in use)
6. Diagnosing Address-Already-in-Use Errors Before Starting a Service
The error bind() to 0.0.0.0:80 failed (98: Address already in use) is one of the most common reasons a deployment fails. The cause is almost always the same: another process, a Docker container through docker-proxy, or a stuck old process of the same application is already holding the port. Instead of blindly restarting the service and hoping for the best, it pays off to run a targeted check with ss before every start, especially in deployment scripts and CI/CD pipelines.
In systemd units, this check can even be automated: an ExecStartPre line checks the port and aborts with a clear error message in the journal instead of the service ending up in an uninformative restart loop. In an emergency this saves valuable minutes of troubleshooting, because it is immediately clear that a port conflict, not a configuration error in the service itself, is the cause.
[Unit]
Description=Custom Magento Queue Consumer
After=network.target mysql.service
[Service]
Type=simple
User=magento
# Fail fast with a clear log line instead of a cryptic bind error
ExecStartPre=/bin/sh -c '! ss -ltn sport = :8082 | grep -q LISTEN'
ExecStart=/usr/bin/php /var/www/magento/bin/magento queue:consumers:start async.operations.all
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
A frequent trigger in containerized environments is a poorly cleaned up docker-compose.yml: a reverse proxy binds port 80 directly to the host, while a second service tries to claim the same host port through an older compose file. docker ps --filter "publish=80" reliably shows which container is already holding the port in this case, before ss even comes into play.
# docker-compose.yml: a leftover container is a common cause
# of "Address already in use" on the host
services:
varnish:
image: varnish:7.5
ports:
- "80:80" # binds host port 80 directly
depends_on:
- nginx
nginx:
image: nginx:1.27
expose:
- "8080"
# Find the container holding the host port before starting nginx natively:
# docker ps --filter "publish=80"
7. Filter Expressions and Connection Statistics
The filter syntax of ss goes well beyond simple port arguments. Expressions such as state established, state time-wait, or combinations with and can be written directly after the options, for example ss -tan state established '( dport = :443 or sport = :443 )' to isolate exactly all active HTTPS connections. These filters are evaluated by the kernel itself, which is considerably faster than downstream filtering with grep or awk, especially on systems with many thousands of connections.
For a quick overview without individual connections, ss -s delivers a compact statistic: total number of sockets broken down by TCP, UDP, and other protocols, plus the distribution of TCP connections across states such as established, closed, orphaned, and timewait. For programmatic processing, for example in a monitoring script, ss -J is a good fit, since it outputs the socket list as JSON and thereby enables robust parsing without fragile column counting.
// ss -J emits machine-readable JSON, stable for scripts and monitoring
$ ss -tlnp -J
{
"sockets": [
{
"local": { "address": "0.0.0.0", "port": 80 },
"peer": { "address": "0.0.0.0", "port": 0 },
"state": "LISTEN",
"process": { "name": "nginx", "pid": 1284 }
},
{
"local": { "address": "127.0.0.1", "port": 9000 },
"peer": { "address": "0.0.0.0", "port": 0 },
"state": "LISTEN",
"process": { "name": "php-fpm8.3", "pid": 2201 }
}
]
}
8. netstat: When the Legacy Tool Still Makes Sense
Despite all its drawbacks, netstat is not worthless. On very minimal systems, old appliances, or legacy container images without iproute2, netstat is sometimes the only tool available. The output of netstat -s has also delivered the same familiar format for decades, with detailed protocol statistics, for example on retransmitted TCP segments or dropped ICMP packets, that many experienced administrators can still read well out of habit.
Things get problematic when old monitoring or deployment scripts break down the text output of netstat line by line with grep and awk. Such scripts break with every small change in column width or localization. Anyone modernizing existing scripts should gradually move them to ss -H, which suppresses the header line, or to ss -J for structured JSON. Both variants deliver a stable, versioned output format that does not change with every distribution update, making automation considerably more robust.
9. ss and netstat Side by Side
For day-to-day work, a direct comparison of the most common commands is worthwhile. Anyone bringing netstat habits from older scripts or from training material will find the corresponding ss equivalent here, including the concrete advantage in production.
| Task | netstat (legacy) | ss (recommended) | Advantage |
|---|---|---|---|
| List listening TCP ports | netstat -tlnp |
ss -tlnp |
Netlink instead of /proc parsing, considerably faster |
| Find the process behind a port | netstat -tulpn | grep :80 |
ss -tlnp sport = :80 |
Kernel filter instead of downstream text search |
| Connection statistics | netstat -s |
ss -s |
More compact, current kernel counters |
| Count established connections | netstat -ant | grep ESTABLISHED | wc -l |
ss -tan state established | wc -l |
No free-text parsing needed |
| List UNIX sockets | netstat -x |
ss -x |
Same filter syntax as for TCP/UDP |
The trend in the table is clear: wherever netstat relies on text processing with grep or awk, ss offers a native filter expression that is evaluated directly by the kernel. That reduces not only the runtime but also the fragility of scripts that depend on stable output.
Mironsoft
Server diagnostics, deployment hardening, and Linux infrastructure
Port conflicts and stuck connections under control?
We analyze your server infrastructure, identify port conflicts and socket leaks, and build resilient pre-checks into your deployment and systemd processes, so address-already-in-use errors no longer show up in production.
Server Audit
Complete analysis of open ports, processes, and connection states
Deployment Hardening
Retrofitting port checks into systemd units and CI/CD pipelines
Monitoring Integration
Feeding ss -J and structured connection data into your existing monitoring systems
10. Summary
ss and netstat solve the same basic task, namely making network connections visible, but ss does so through netlink directly inside the kernel and is therefore faster and more reliable than the text-based netstat. Connection states such as LISTEN, ESTABLISHED, and TIME-WAIT describe normal phases of a TCP connection, while accumulating CLOSE-WAIT entries can almost always be traced back to a bug in the application. With ss -tlnp and targeted filter expressions, you can determine within seconds which process owns a port, well before a deployment aborts with a cryptic bind error.
The biggest practical benefit comes from building this check into deployment scripts and systemd units preemptively, not only reaching for it during debugging. An ExecStartPre check that immediately flags an occupied port with a clear message in the journal regularly saves valuable minutes over manually debugging a failed service start. netstat remains relevant as a fallback on minimal systems, but actively maintained scripts should gradually move to ss -H or ss -J.
ss and netstat: The Essentials at a Glance
ss instead of netstat
Netlink-based, considerably faster with many sockets, and part of the preinstalled iproute2 package on almost every distribution.
Reading states correctly
TIME-WAIT is normal after a connection ends, accumulating CLOSE-WAIT entries almost always point to a socket leak.
Finding the process per port
sudo ss -tlnp sport = :PORT shows the process name and PID directly, without text filtering via grep.
Preventing conflicts
A port check as ExecStartPre in systemd units or as a pre-step in a deployment script prevents cryptic bind errors.