I have typed ss thousands of times. For most of those, I was really only asking it one question: "what is listening on this box, and who is talking to it?" I would run ss -tlnp, skim the output, and move on. The columns to the right of the addresses were noise I had trained myself to ignore.
That is a poor way to know a tool. So this is me teaching it to myself, out loud, one column and one flag at a time. If you already live in ss, you can skip to the state machine or the filtering sections, which are the parts most people (me included) underuse.
A quick note on what ss even is. It ships in iproute2, the same package that gives you ip. It reads socket information straight from the kernel over a netlink socket (sock_diag), which is why it is faster than netstat. netstat walks /proc/net/tcp and friends, parsing text files line by line; ss asks the kernel directly. The name stands for "socket statistics". It is the tool you should reach for now that netstat is deprecated on most distributions.
Installing ss
You almost certainly already have it. iproute2 is a base package on essentially every modern Linux system, so ss is present out of the box on anything you are likely to SSH into. If it is somehow missing:
# Debian, Ubuntu
apt install iproute2
# RHEL, CentOS, Fedora, Rocky, Alma
dnf install iproute
# Alpine
apk add iproute2
# Arch
pacman -S iproute2
Confirm the version, because the output format and some flags have shifted over the years:
$ ss --version
ss utility, iproute2-6.1.0
Everything below reflects a reasonably recent iproute2 (5.x and up). If you are on something ancient, the columns are the same but a few of the richer flags (--cgroup, --tos) may be absent.
Running it bare
Start with no arguments at all. This is where most people never look, because the output is a wall of sockets you did not ask about.
$ ss
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port
u_str ESTAB 0 0 /run/systemd/journal/stdout 24807 * 24806
u_str ESTAB 0 0 * 0 * 0
tcp ESTAB 0 0 192.168.1.50:ssh 192.168.1.10:52814
tcp ESTAB 0 0 192.168.1.50:47320 140.82.113.25:https
Two things surprise people here. First, bare ss shows every socket family it knows about, not just TCP. Those u_str rows are Unix domain sockets, the local IPC channels that daemons use to talk to each other. Second, it shows only connected sockets by default. Nothing that is merely listening appears until you ask for it with -l or -a. So the default view answers "who is currently talking", which is a reasonable default, and also the opposite of what I usually want.
Let me walk the columns, because every other invocation is just a variation on these.
The columns
Netid
The socket family. You will mostly see:
tcp,udpfor the internet transports you expect.u_str,u_dgr,u_seqfor Unix domain sockets (stream, datagram, seqpacket). These are the local sockets under paths like/run/....nlfor netlink, the kernel's own control-plane sockets.rawfor raw IP sockets (thinkping).p_raw,p_dgrfor packet sockets (thinktcpdump).
When you constrain the output to a single family with a flag like -t, this column disappears, since it would say the same thing on every row.
State
The lifecycle stage of the socket. For TCP this is a rich set drawn straight from the TCP state machine, and it is worth its own section below. For the connectionless families it collapses to two useful values: ESTAB when a socket has a peer wired up, and UNCONN when it does not. A bound UDP socket that has not connect()ed to a specific peer sits in UNCONN, which reads oddly the first time you see it, since UDP has no connections in the TCP sense.
Recv-Q and Send-Q
Here is the field I ignored for years, and the one with a genuine trap in it: these two columns mean completely different things depending on whether the socket is listening or connected.
For a connected socket:
Recv-Qis the number of bytes sitting in the kernel's receive buffer that your application has notread()yet. A number that climbs and stays high means your program is not draining its socket fast enough. The data arrived; nobody picked it up.Send-Qis the number of bytes sitting in the send buffer that the kernel has handed to the network but the peer has not acknowledged. A persistently highSend-Qpoints the finger the other way: the remote end, or the path to it, is not keeping up.
For a listening socket the same two columns are repurposed entirely:
Recv-Qis the number of connections currently sitting in the accept queue, fully established by the kernel and waiting for your application to callaccept().Send-Qis the maximum size of that accept queue, which is the backlog you passed tolisten(), capped bynet.core.somaxconn.
This second meaning is the useful one and almost nobody knows it. If you are debugging a service that drops connections under load, look at a listening socket's Recv-Q against its Send-Q:
$ ss -tln
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
Send-Q of 511 on port 80 is nginx's default backlog. If under load you saw Recv-Q creeping toward 511 on that row, your application is accepting connections slower than they arrive, and the kernel is about to start refusing them. That is a real diagnosis you can make at a glance, and it is invisible if you read these columns the way you read them on a connected socket.
Local Address:Port and Peer Address:Port
The two endpoints. A few conventions are worth internalizing:
*is the wildcard.*:*as a peer means "anything", which is what you see on listening sockets, since they have no single peer yet.0.0.0.0and[::]are the wildcard addresses for "all interfaces" on IPv4 and IPv6 respectively.- By default
ssresolves both the address (to a hostname) and the port (to a service name from/etc/services). That is why you seesshandhttpsinstead of22and443above. It is friendly and it is slow, since resolution can block on DNS. The-nflag turns it off, and you will want it off more often than on. - For IPv6 the address is bracketed, so a socket looks like
[::1]:443. The port always follows the last colon, which is exactly why the brackets exist. - For Unix sockets the "address" is a filesystem path (or an abstract name), and the trailing number after it is the socket's inode. That is how you correlate the two ends of a Unix socket pair: matching inodes.
That is the entire default output. Everything else ss does is either filtering these rows down or hanging more detail off of them.
Socket states
TCP is a state machine, and ss shows you which state each connection is in. This is the ss equivalent of learning what R, S, D, and Z mean in a process list: once you know the states, the output starts telling you stories instead of just facts.
Here are the states you will actually encounter, roughly in the order a connection lives through them:
LISTENA server socket waiting for incoming connections. It has a local address and port but no peer. This is what you are looking for when you ask "what is running on this machine".SYN-SENTYou initiated a connection (connect()) and sent the opening SYN, but the other side has not answered yet. A pile of these means you are trying to reach something that is not responding: a down host, a dropped packet, a firewall silently eating your SYNs.SYN-RECVThe mirror image. You received a SYN, replied with SYN-ACK, and are waiting for the final ACK. A flood ofSYN-RECVfrom many different peers is the classic signature of a SYN flood, or of clients disappearing mid-handshake.ESTABThe connection is open and data can flow both ways. The steady state, and the bulk of a healthy server's rows.FIN-WAIT-1andFIN-WAIT-2You calledclose()and sent a FIN; now you are waiting for the peer to acknowledge it (FIN-WAIT-1) and then to send its own FIN (FIN-WAIT-2). Connections stuck inFIN-WAIT-2usually mean the remote application closed its side lazily, or not at all.CLOSE-WAITThe peer closed first, the kernel acknowledged it, and now the kernel is waiting for your application to callclose(). This one is a smoking gun. A growing count ofCLOSE-WAITsockets almost always means a bug in your program: it is leaking connections, forgetting to close file descriptors after the other end hung up. The kernel cannot clean these up for you; only yourclose()can.LAST-ACKYou sent your FIN after being inCLOSE-WAITand are waiting for the final acknowledgement. Transient.TIME-WAITThe connection is closed, but the socket lingers for a while (typically 2 minutes, twice the maximum segment lifetime) to absorb any stray late packets and to make sure the peer received the final ACK. Thousands ofTIME-WAITsockets on a busy server are normal and usually harmless; they are the residue of connections that closed cleanly. They only become a problem when you exhaust ephemeral ports.CLOSINGBoth sides sent FINs at nearly the same moment. Rare, and transient.UNCONNNot a TCP state at all. It is whatssprints for a bound-but-not-connected socket, which you see on UDP and on listening sockets of families that do not have aLISTENconcept.
The practical payoff: the distribution of states is a health readout. Mostly ESTAB with a scattering of TIME-WAIT is a happy machine. A wall of CLOSE-WAIT is a leaking application. A wall of SYN-SENT is a reachability problem. A wall of SYN-RECV is something attacking your handshake. You can produce that distribution in one line, which I will get to under summary statistics.
Choosing what to show
Now the flags. The first group narrows ss down to the family and lifecycle you care about, and you will combine two or three of them almost every time.
Protocol selectors:
ss -t # TCP only
ss -u # UDP only
ss -x # Unix domain sockets only
ss -w # raw sockets
ss -4 # IPv4 only
ss -6 # IPv6 only
These stack. ss -tu shows TCP and UDP together; ss -t4 shows IPv4 TCP.
Lifecycle selectors:
ss -l # listening sockets only
ss -a # all sockets (listening and connected)
Remember that bare ss shows only connected sockets. -l flips that to only listening, and -a shows both. So the two invocations I actually use constantly are:
ss -tlnp # what is listening (TCP), numeric, with the owning process
ss -tnp # who is connected right now (TCP), numeric, with the process
That is the canonical "audit this box" command and the canonical "watch live traffic" command, respectively. -n for numeric is doing a lot of quiet work in both: it stops ss from blocking on reverse DNS for every foreign address, which on a server talking to the wider internet is the difference between instant and interminable.
Making it readable
A few flags exist purely to shape the output for eyes or for scripts.
-nnumeric. Skip name resolution for both addresses and ports. Faster, and unambiguous.-rresolve. The opposite: force resolution of numeric addresses back to names. The default already resolves, so you reach for this mostly when combined with other tools.-Hno header. Drop the header row. Essential when you are piping intoawkorwcand do not want to special-case the first line.-Ooneline. Keep each socket's full record on a single line. The detail flags like-inormally wrap continuation data onto an indented second line;-Oun-wraps it so each socket is one grep-able record.-Qno queues. Suppress the Recv-Q and Send-Q columns when you do not care about them and want the addresses to line up more tidily.
For example, "count how many established connections there are" becomes clean with -H:
$ ss -tnH state established | wc -l
47
No header to subtract, no service-name resolution to wait on.
Who owns the socket
A socket without its owning process is half a fact. The -p flag closes that gap, and a couple of siblings add identity detail.
$ ss -tlnp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=812,fd=3))
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=1123,fd=6),("nginx",pid=1122,fd=6))
The Process column reads users:(("name",pid=N,fd=M)), and it can list several holders when a socket is shared across forked workers, as nginx does above. fd is the file descriptor number inside that process, which is occasionally handy when you go spelunking in /proc/PID/fd.
One caveat that trips everyone up once: -p only shows processes you have permission to see. Sockets owned by other users, including most system daemons, show up with an empty Process column unless you run ss as root. If your process column is mysteriously blank, prefix with sudo.
Two related flags add identity:
-eextended. Appends the owninguid, the socketinode, and askcookie that uniquely identifies the socket to the kernel. Useful for correlating a socket across tools or across successivessruns.-Zand-zcontext. On SELinux systems, show the security context of the process (-Z) or of the socket itself (-z).
Filtering by state
Everything so far narrows by family and by process. The more powerful axis is filtering by TCP state, and ss has a compact language for it. You write state NAME after the flags:
ss -tn state established
ss -tn state time-wait
ss -tn state close-wait
The state names are exactly the ones from the state machine above, lowercased and hyphenated: established, syn-sent, syn-recv, fin-wait-1, fin-wait-2, time-wait, closed, close-wait, last-ack, listening, closing.
What elevates this from convenient to genuinely useful are the aggregate aliases, which let you ask about whole categories of state at once:
connectedevery state exceptlisteningandclosed. "Show me sockets that have a peer."synchronizedthe connected states exceptsyn-sent. "Show me sockets past the handshake."bucketthe minisocket states,time-waitandsyn-recv, which the kernel keeps in a compact table rather than as full sockets.bigthe inverse ofbucket, meaning the full-socket states.allevery state, equivalent to-a.
So ss -tn state connected is a precise way to say "real conversations only, no listeners", and ss -tn state bucket isolates the lightweight leftovers that a busy server accumulates.
You can also negate. ss -tn exclude established gives you everything that is not in the steady state, which is a fast way to spot connections that are stuck mid-lifecycle.
Filtering by address and port
The second filter language matches on the endpoints. The predicates are src and dst for addresses, sport and dport for ports, and they compose with boolean operators.
The simplest form matches a port:
ss -tn dport = :443 # connections whose remote port is 443
ss -tn sport = :22 # connections whose local port is 22
Note the leading colon on :443. The general host syntax is ADDRESS:PORT, so a bare :443 means "any address, port 443". You can pin the address too:
ss -tn dst 140.82.113.25
ss -tn dst 140.82.113.25:443
ss -tn src 192.168.1.0/24 # CIDR ranges work
ss -tn 'dst != 127.0.0.0/8' # everything not loopback
Ports understand comparison operators, not only equality, which is how you filter port ranges. The operators are <, <=, =, >=, >, and !=, with word aliases (eq, ne, gt, lt, ge, le) if you prefer them or if your shell is fighting you over the symbols:
ss -tn 'sport >= :1024' # local ephemeral ports
ss -tn 'dport < :1024' # connections out to well-known ports
And you combine predicates with and / or (also spelled && / ||), which is where this turns into a real query language. Quote the whole expression so the shell leaves your operators alone:
# established connections to a specific host on the HTTPS port
ss -tn 'dst 140.82.113.25 and dport = :443'
# anything talking to port 80 or 443
ss -tn 'dport = :80 or dport = :443'
Combine the two filter languages freely. State filter and address filter sit side by side:
ss -tn state established dst 10.0.0.0/8
That reads as "established TCP connections to the private 10-net", numeric, and it is the sort of thing you would otherwise assemble out of grep pipelines that break the moment an address happens to contain your search string.
Going deeper on a connection
Everything above is about which sockets. The next flags are about knowing one socket in depth, which is where ss quietly becomes a performance tool.
-i shows the kernel's internal TCP information for each connection: the numbers TCP itself is using to decide how fast to send.
$ ss -tin dst 140.82.113.25
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 0 192.168.1.50:47320 140.82.113.25:443
cubic wscale:7,7 rto:216 rtt:14.5/7.25 mss:1448 cwnd:10 ssthresh:7
bytes_sent:8192 bytes_acked:8193 bytes_received:20480 segs_out:24 segs_in:30
send 8.0Mbps lastsnd:44 pacing_rate 16.0Mbps delivery_rate 5.2Mbps
rcv_rtt:15 rcv_space:14480 retrans:0/2 rcv_ooo:0
That indented block is dense, so here are the fields that earn their keep:
cubicthe congestion-control algorithm this socket is using.cubicis the modern Linux default; you might also seebbron tuned hosts.rtt:14.5/7.25the smoothed round-trip time and its variance, in milliseconds. This is your latency to the peer, measured by the kernel from real traffic rather than by aping.cwnd:10the congestion window, in segments. How many segments TCP will keep in flight before waiting for an ACK. This is the single biggest lever on throughput.ssthresh:7the slow-start threshold. Below it TCP grows the window exponentially; above it, linearly.rto:216the retransmission timeout in milliseconds: how long TCP waits for an ACK before assuming loss and resending.retrans:0/2current and total retransmissions. A nonzero total is not alarming on its own, but a climbing total on a connection that should be clean means packet loss on the path.send 8.0Mbps,delivery_rate,pacing_ratethe kernel's own estimates of how fast this connection is actually moving data. When someone reports "the download is slow", this line often tells you whether the bottleneck is the window, the loss rate, or genuinely the link.
You do not need to memorize all of it. Knowing that rtt, cwnd, and retrans live here, one command away, changes how you debug a slow connection. It stops being a mystery and becomes three numbers.
-o shows timer information: which kernel timer is armed on the socket and when it fires.
$ ss -ton state established
State Recv-Q Send-Q Local Address:Port Peer Address:Port Timer
ESTAB 0 0 192.168.1.50:ssh 192.168.1.10:52814 timer:(keepalive,18min,0)
The tuple is (name, time-until-fire, retransmit-count). A keepalive timer is the connection idling; a on (retransmission) timer with a rising count is a connection actively fighting to get data through. Seeing which timer is armed tells you what the socket is currently worried about.
-m shows socket memory usage: how many bytes the kernel has allocated to this socket's buffers.
$ ss -tm state established
ESTAB 0 0 192.168.1.50:ssh 192.168.1.10:52814
skmem:(r0,rb131072,t0,tb87040,f0,w0,o0,bl0,d0)
The skmem fields break down receive and send buffer usage. rb and tb are the buffer limits; r and t are current usage; d counts drops. If you are chasing memory pressure from many sockets, this is where the accounting lives.
Summary statistics
-s steps back and prints totals instead of individual sockets. It is the fastest possible answer to "what is the shape of this machine's networking".
$ ss -s
Total: 176
TCP: 8 (estab 4, closed 1, orphaned 0, timewait 1)
Transport Total IP IPv6
RAW 1 0 1
UDP 6 4 2
TCP 7 4 3
INET 14 8 6
FRAG 0 0 0
The top line is every socket the kernel holds, across all families. The TCP: line breaks the TCP sockets down by state, which is the health readout I described in the state machine section, available instantly. orphaned is worth knowing: those are sockets with no owning process, usually mid-teardown, and a large orphan count can indicate an application that is closing sockets without draining them.
If you only ever learn two ss commands, make them ss -tlnp for "what is on this box" and ss -s for "how is this box doing".
A few real recipes
The point of learning the pieces is to combine them without thinking. These are the ones I reach for.
What is listening, and who owns it:
ss -tlnp
Count established connections, grouped by the remote host, to find who is hammering you:
ss -tnH state established | awk '{print $4}' | cut -d: -f1 | sort | uniq -c | sort -rn
Count connections by state, the health snapshot as a table:
ss -tanH | awk '{print $1}' | sort | uniq -c | sort -rn
Find the process holding a specific port when "address already in use" is ruining your afternoon:
ss -tlnp 'sport = :8080'
Watch TIME-WAIT accumulation on a busy server, to decide whether you are actually near port exhaustion:
ss -tn state time-wait | wc -l
See only the connections that are stuck somewhere other than established, which surfaces leaks and reachability problems in one glance:
ss -tn exclude established exclude listening
Everyone connected to your SSH port right now, numeric and fast:
ss -tnp 'sport = :22'
Closing
ss rewards the same investment htop does. The bare output looks like noise until you know what each column is telling you, and then the same command becomes a diagnosis. The parts I underused for years turned out to be the valuable ones: Recv-Q and Send-Q on a listening socket, the state aliases like connected and bucket, and the -i block that turns a slow connection from a complaint into three legible numbers.
I did not cover the exotic families (SCTP, MPTCP, vsock, XDP) or the kill switch (-K, which forcibly tears a socket down and which you should be quite sure about before you run). The ss(8) man page is genuinely good and worth a slow read once you have the shape of the tool in your head. Start with ss -tlnp, get curious about a column you have been ignoring, and follow it down.