{"id":196,"date":"2026-07-29T09:00:00","date_gmt":"2026-07-29T09:00:00","guid":{"rendered":"https:\/\/backendside.com\/blog\/?p=196"},"modified":"2026-07-29T18:07:47","modified_gmt":"2026-07-29T18:07:47","slug":"find-process-using-port-linux","status":"publish","type":"post","link":"https:\/\/backendside.com\/blog\/2026\/07\/29\/find-process-using-port-linux\/","title":{"rendered":"Which Process Is Using That Port? Mapping a Listening Port to a PID on Linux"},"content":{"rendered":"<p class=\"lead\">You restart a service and it refuses to come up: <em>bind: Address already in use<\/em>. Or you run a security scan and find port 9001 open on a box where nothing is supposed to be listening on 9001. Both problems reduce to the same question&mdash;<strong>which process owns this listening socket?<\/strong> Linux will tell you, and it takes one command. Here is that command, three alternatives for when it is not installed, and what to do in the awkward cases where a port seems to belong to nobody at all.<\/p>\n<h2>The short answer<\/h2>\n<p>On any reasonably modern distribution, this is the whole trick:<\/p>\n<pre style=\"background:#1e1d1b;color:#eceae6;padding:1rem 1.15rem;border-radius:8px;overflow-x:auto;font-size:.85rem;line-height:1.5;\"><code>sudo ss -ltnp<\/code><\/pre>\n<p>That lists every TCP socket in the LISTEN state, without resolving names, with the owning process attached. Output looks like this:<\/p>\n<pre style=\"background:#1e1d1b;color:#eceae6;padding:1rem 1.15rem;border-radius:8px;overflow-x:auto;font-size:.85rem;line-height:1.5;\"><code>State   Recv-Q  Send-Q   Local Address:Port   Peer Address:Port  Process\nLISTEN  0       128            0.0.0.0:22          0.0.0.0:*      users:((\"sshd\",pid=811,fd=3))\nLISTEN  0       511            0.0.0.0:80          0.0.0.0:*      users:((\"nginx\",pid=1187,fd=6),(\"nginx\",pid=1186,fd=6))\nLISTEN  0       4096         127.0.0.1:3306        0.0.0.0:*      users:((\"mariadbd\",pid=942,fd=25))\nLISTEN  0       4096                  *:8080               *:*    users:((\"backendsidemon\",pid=1502,fd=9))<\/code><\/pre>\n<p>The last column is the answer. <code>users:((\"nginx\",pid=1187,fd=6))<\/code> means: the process named <code>nginx<\/code>, PID 1187, is holding this socket on file descriptor 6. Port 80 shows two entries because a master process and a worker both hold the listening descriptor&mdash;that is normal for pre-forking servers.<\/p>\n<p>To look at one port instead of all of them, use a filter rather than piping to <code>grep<\/code>&mdash;it avoids matching the port number in some other column:<\/p>\n<pre style=\"background:#1e1d1b;color:#eceae6;padding:1rem 1.15rem;border-radius:8px;overflow-x:auto;font-size:.85rem;line-height:1.5;\"><code>sudo ss -ltnp 'sport = :8080'<\/code><\/pre>\n<h3>The flags, decoded<\/h3>\n<table style=\"width:100%;border-collapse:collapse;margin:1rem 0;font-size:.9rem;\">\n<thead>\n<tr style=\"background:#f3f5fa;\">\n<th style=\"text-align:left;padding:.55rem .7rem;border:1px solid #e4e2de;\">Flag<\/th>\n<th style=\"text-align:left;padding:.55rem .7rem;border:1px solid #e4e2de;\">What it does<\/th>\n<th style=\"text-align:left;padding:.55rem .7rem;border:1px solid #e4e2de;\">Why you want it<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"padding:.55rem .7rem;border:1px solid #e4e2de;\"><code>-t<\/code> \/ <code>-u<\/code><\/td>\n<td style=\"padding:.55rem .7rem;border:1px solid #e4e2de;\">TCP \/ UDP sockets<\/td>\n<td style=\"padding:.55rem .7rem;border:1px solid #e4e2de;\">Filters out the pile of Unix domain sockets<\/td>\n<\/tr>\n<tr>\n<td style=\"padding:.55rem .7rem;border:1px solid #e4e2de;\"><code>-l<\/code><\/td>\n<td style=\"padding:.55rem .7rem;border:1px solid #e4e2de;\">Listening sockets only<\/td>\n<td style=\"padding:.55rem .7rem;border:1px solid #e4e2de;\">A server binding a port is what you are hunting<\/td>\n<\/tr>\n<tr>\n<td style=\"padding:.55rem .7rem;border:1px solid #e4e2de;\"><code>-n<\/code><\/td>\n<td style=\"padding:.55rem .7rem;border:1px solid #e4e2de;\">Numeric &mdash; no name resolution<\/td>\n<td style=\"padding:.55rem .7rem;border:1px solid #e4e2de;\">Shows <code>:8080<\/code>, not <code>:http-alt<\/code>; also much faster<\/td>\n<\/tr>\n<tr>\n<td style=\"padding:.55rem .7rem;border:1px solid #e4e2de;\"><code>-p<\/code><\/td>\n<td style=\"padding:.55rem .7rem;border:1px solid #e4e2de;\">Show the owning process<\/td>\n<td style=\"padding:.55rem .7rem;border:1px solid #e4e2de;\">The entire point &mdash; needs root for other users&#8217; processes<\/td>\n<\/tr>\n<tr>\n<td style=\"padding:.55rem .7rem;border:1px solid #e4e2de;\"><code>-a<\/code><\/td>\n<td style=\"padding:.55rem .7rem;border:1px solid #e4e2de;\">All sockets, not just listening<\/td>\n<td style=\"padding:.55rem .7rem;border:1px solid #e4e2de;\">Use when chasing an established connection instead<\/td>\n<\/tr>\n<tr>\n<td style=\"padding:.55rem .7rem;border:1px solid #e4e2de;\"><code>-e<\/code><\/td>\n<td style=\"padding:.55rem .7rem;border:1px solid #e4e2de;\">Extended info, including the socket inode<\/td>\n<td style=\"padding:.55rem .7rem;border:1px solid #e4e2de;\">The escape hatch when <code>-p<\/code> gives you nothing<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2>Why you almost always need sudo<\/h2>\n<p>Run <code>ss -ltnp<\/code> without root and the Process column will be blank for everything you do not own. The kernel only lets you map a socket back to a PID if the process is yours or you have <code>CAP_NET_ADMIN<\/code>. This trips people up constantly: the port is clearly listed as LISTEN, the process field is empty, and it looks like a ghost. It is not a ghost&mdash;it is a permissions problem. Add <code>sudo<\/code> and try again before you go any further.<\/p>\n<div style=\"border:1px solid #c5d3f8;background:linear-gradient(135deg,#eef2fd 0%,#ffffff 72%);border-radius:14px;padding:1.5rem 1.65rem;margin:2rem 0;\">\n<div style=\"font-size:.7rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#2d5be3;margin-bottom:.55rem;\">&#128295; BackendSide Tool<\/div>\n<h4 style=\"margin:0 0 .45rem;font-size:1.15rem;color:#1a1916;font-weight:700;\">BackendSideMon &mdash; See Every Listener and Its Process in a Browser<\/h4>\n<p style=\"margin:0 0 1.05rem;color:#3d3c38;font-size:.92rem;line-height:1.65;\">Running <code>ss -ltnp<\/code> is fine when you have one server and an SSH session already open. <strong>BackendSideMon<\/strong> gives you the same answer as a live web dashboard: active listeners and connections with local and remote IPs, ports and states, alongside the running processes and their full executable paths. It installs as a systemd service on RHEL-based and Debian\/Ubuntu-based distributions and serves the dashboard on port 8080.<\/p>\n<p>  <a href=\"https:\/\/backendside.com\/backendsidemon.php\" style=\"display:inline-flex;align-items:center;gap:.4rem;background:#2d5be3;color:#ffffff;font-weight:600;font-size:.85rem;padding:.6rem 1.2rem;border-radius:6px;text-decoration:none;\">Explore BackendSideMon &rarr;<\/a>\n<\/div>\n<h2>When <code>ss<\/code> is not there: three fallbacks<\/h2>\n<h3>1. <code>lsof<\/code> &mdash; the most readable output<\/h3>\n<p><code>lsof<\/code> treats sockets as what they are on Linux: open files. Ask it which files match an internet address:<\/p>\n<pre style=\"background:#1e1d1b;color:#eceae6;padding:1rem 1.15rem;border-radius:8px;overflow-x:auto;font-size:.85rem;line-height:1.5;\"><code>sudo lsof -i :8080 -sTCP:LISTEN -P -n\n\nCOMMAND          PID  USER   FD   TYPE DEVICE SIZE\/OFF NODE NAME\nbackendsidemon  1502  root    9u  IPv6  28841      0t0  TCP *:8080 (LISTEN)<\/code><\/pre>\n<p><code>-P<\/code> and <code>-n<\/code> keep ports and addresses numeric, and <code>-sTCP:LISTEN<\/code> drops established connections so you only see the binding process. For scripting, <code>-t<\/code> prints bare PIDs and nothing else:<\/p>\n<pre style=\"background:#1e1d1b;color:#eceae6;padding:1rem 1.15rem;border-radius:8px;overflow-x:auto;font-size:.85rem;line-height:1.5;\"><code>sudo lsof -t -i :8080 -sTCP:LISTEN\n1502<\/code><\/pre>\n<h3>2. <code>fuser<\/code> &mdash; fastest when you only want the PID<\/h3>\n<pre style=\"background:#1e1d1b;color:#eceae6;padding:1rem 1.15rem;border-radius:8px;overflow-x:auto;font-size:.85rem;line-height:1.5;\"><code>sudo fuser 8080\/tcp\n8080\/tcp:             1502\n\nsudo fuser -v 8080\/tcp\n                     USER        PID ACCESS COMMAND\n8080\/tcp:            root       1502 F....  backendsidemon<\/code><\/pre>\n<p><code>fuser<\/code> also has <code>-k<\/code>, which kills whatever it finds. It is genuinely useful and genuinely dangerous&mdash;<code>sudo fuser -k 8080\/tcp<\/code> will happily terminate a production database if that is what answered. Always run it without <code>-k<\/code> first and read the output.<\/p>\n<h3>3. <code>netstat<\/code> &mdash; the one in every old runbook<\/h3>\n<pre style=\"background:#1e1d1b;color:#eceae6;padding:1rem 1.15rem;border-radius:8px;overflow-x:auto;font-size:.85rem;line-height:1.5;\"><code>sudo netstat -tulpn | grep ':8080'\ntcp6  0  0 :::8080   :::*   LISTEN   1502\/backendsidemon<\/code><\/pre>\n<p><code>netstat<\/code> comes from the deprecated <code>net-tools<\/code> package and is not installed by default on most current distributions. It reads <code>\/proc\/net\/tcp<\/code> by walking every process&#8217;s file descriptors, which is noticeably slow on a busy machine. Learn <code>ss<\/code>; keep <code>netstat<\/code> for the servers you inherited.<\/p>\n<h2>From PID to the whole story<\/h2>\n<p>A PID and a process name are rarely the end of the investigation&mdash;&#8221;python3&#8243; or &#8220;java&#8221; tells you nothing. Once you have the number, <code>\/proc<\/code> gives you everything else:<\/p>\n<pre style=\"background:#1e1d1b;color:#eceae6;padding:1rem 1.15rem;border-radius:8px;overflow-x:auto;font-size:.85rem;line-height:1.5;\"><code># Full command line, owner, parent, and how long it has been up\nps -p 1502 -o pid,ppid,user,etime,cmd\n\n# The actual binary on disk (survives a renamed process)\nsudo ls -l \/proc\/1502\/exe\n\n# Its working directory - often reveals which deployment it belongs to\nsudo ls -l \/proc\/1502\/cwd\n\n# The raw argv, NUL-separated\nsudo tr '\u0000' ' ' &lt; \/proc\/1502\/cmdline; echo\n\n# Which systemd unit or container owns it\ncat \/proc\/1502\/cgroup\nsystemctl status 1502<\/code><\/pre>\n<p>That last one is the most useful and the least known: <code>systemctl status &lt;pid&gt;<\/code> resolves a PID to its owning unit, so you find out that port 8080 belongs to <code>backendsidemon.service<\/code> and can stop it properly instead of killing it.<\/p>\n<h3>The manual method: socket inodes<\/h3>\n<p>If <code>-p<\/code> gives you nothing and you have already ruled out permissions, you can do the mapping by hand. Every socket has an inode number, and every process holding it has a symlink to <code>socket:[inode]<\/code> in its file-descriptor directory:<\/p>\n<pre style=\"background:#1e1d1b;color:#eceae6;padding:1rem 1.15rem;border-radius:8px;overflow-x:auto;font-size:.85rem;line-height:1.5;\"><code>$ sudo ss -ltne 'sport = :8080'\nLISTEN 0 4096 *:8080 *:*  ino:28841 sk:3 cgroup:\/system.slice\/backendsidemon.service\n\n$ sudo find \/proc\/[0-9]*\/fd -lname 'socket:[28841]' 2&gt;\/dev\/null\n\/proc\/1502\/fd\/9<\/code><\/pre>\n<p>The path contains the PID. This is exactly what <code>ss<\/code>, <code>lsof<\/code> and <code>netstat<\/code> do internally&mdash;it is worth seeing once, because it explains why these tools need root and why they get slow on machines with tens of thousands of open descriptors.<\/p>\n<h2>The awkward cases<\/h2>\n<h3>The port is listed but the process column is empty<\/h3>\n<p>In order of likelihood: you forgot <code>sudo<\/code>; the socket belongs to a process in a different network namespace (a container); or the listener is kernel-side&mdash;NFS and some tunnelling code bind ports with no userspace process behind them at all.<\/p>\n<h3>Nothing is listening, but the bind still fails<\/h3>\n<p>Check for lingering sockets in <code>TIME_WAIT<\/code> from the previous instance:<\/p>\n<pre style=\"background:#1e1d1b;color:#eceae6;padding:1rem 1.15rem;border-radius:8px;overflow-x:auto;font-size:.85rem;line-height:1.5;\"><code>sudo ss -tan state time-wait '( sport = :8080 or dport = :8080 )'<\/code><\/pre>\n<p>Also worth checking: the port may fall inside the ephemeral range and have been grabbed by an outbound connection. Compare against <code>sysctl net.ipv4.ip_local_port_range<\/code>&mdash;if your service wants a port inside that window, add it to <code>net.ipv4.ip_local_reserved_ports<\/code> so the kernel stops handing it out to clients.<\/p>\n<h3>You see an IPv6 line but no IPv4 line<\/h3>\n<p>A single socket bound to <code>[::]<\/code> serves both families on a dual-stack system, so one <code>tcp6<\/code> row can absolutely be the thing answering your IPv4 requests. The reverse also matters: <code>127.0.0.1:3306<\/code> means loopback only, while <code>0.0.0.0:3306<\/code> means every interface on the box. When you are auditing exposure, the bind address matters more than the port number.<\/p>\n<h3>The listener is inside a container<\/h3>\n<p>Containers have their own network namespace, so a process listening inside one may be invisible to <code>ss<\/code> on the host&mdash;or show up as a proxy process rather than the real service. List the namespaces, then look inside the one you care about:<\/p>\n<pre style=\"background:#1e1d1b;color:#eceae6;padding:1rem 1.15rem;border-radius:8px;overflow-x:auto;font-size:.85rem;line-height:1.5;\"><code>lsns -t net\nsudo nsenter -t 4711 -n ss -ltnp   # 4711 = a PID inside the target namespace<\/code><\/pre>\n<h3>The owner is systemd itself<\/h3>\n<p>With socket activation, systemd holds the listening socket and only starts the real service when the first connection arrives. Until then the port genuinely belongs to PID 1. <code>systemctl list-sockets<\/code> shows every such socket and the unit it will hand off to.<\/p>\n<h2>A repeatable troubleshooting order<\/h2>\n<ol>\n<li><code>sudo ss -ltnp 'sport = :PORT'<\/code> &mdash; the answer, 90% of the time.<\/li>\n<li>No process shown? Confirm you used <code>sudo<\/code>, then check <code>lsns -t net<\/code> for containers.<\/li>\n<li>Got a PID? Run <code>systemctl status PID<\/code> to find the owning unit, and <code>ls -l \/proc\/PID\/exe<\/code> to see the real binary.<\/li>\n<li>Stop it the right way &mdash; <code>systemctl stop &lt;unit&gt;<\/code> beats <code>kill<\/code>, which beats <code>fuser -k<\/code>.<\/li>\n<li>Nothing listening but the bind fails? Look for <code>TIME_WAIT<\/code> and check the ephemeral port range.<\/li>\n<\/ol>\n<div style=\"border:1px solid #c5d3f8;background:linear-gradient(135deg,#eef2fd 0%,#ffffff 72%);border-radius:14px;padding:1.5rem 1.65rem;margin:2rem 0;\">\n<div style=\"font-size:.7rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#2d5be3;margin-bottom:.55rem;\">&#128295; BackendSide Tool<\/div>\n<h4 style=\"margin:0 0 .45rem;font-size:1.15rem;color:#1a1916;font-weight:700;\">Stop SSH-ing Into Servers Just to Run <code>ss<\/code><\/h4>\n<p style=\"margin:0 0 1.05rem;color:#3d3c38;font-size:.92rem;line-height:1.65;\">Port audits get tedious once you have more than a couple of machines. <strong>BackendSideMon<\/strong> runs as a lightweight monitoring service on Windows Server and Linux and puts the whole picture in one dashboard &mdash; TCP\/UDP\/ICMP packet statistics, running processes with their executable paths, every active listener and connection, and failed SSH login tracking. There is an optional Android client too, so you can check a fleet of servers from your phone.<\/p>\n<p>  <a href=\"https:\/\/backendside.com\/backendsidemon.php\" style=\"display:inline-flex;align-items:center;gap:.4rem;background:#2d5be3;color:#ffffff;font-weight:600;font-size:.85rem;padding:.6rem 1.2rem;border-radius:6px;text-decoration:none;\">Explore BackendSideMon &rarr;<\/a>\n<\/div>\n<h2>Key takeaways<\/h2>\n<ul>\n<li><strong><code>sudo ss -ltnp<\/code> is the command to memorise.<\/strong> Listening TCP sockets, numeric, with the owning process&mdash;everything else is a fallback.<\/li>\n<li><strong>Without root, the process column is empty.<\/strong> That is a permissions artefact, not a mystery process.<\/li>\n<li><strong><code>lsof -i :PORT -sTCP:LISTEN -P -n<\/code> is the friendliest alternative<\/strong>, <code>fuser<\/code> the quickest, and <code>netstat -tulpn<\/code> the one your old runbooks assume.<\/li>\n<li><strong>A PID is a starting point, not an answer.<\/strong> <code>\/proc\/PID\/exe<\/code>, <code>\/proc\/PID\/cwd<\/code> and <code>systemctl status PID<\/code> turn &#8220;java&#8221; into a named service you can actually stop.<\/li>\n<li><strong>Read the bind address, not just the port.<\/strong> <code>127.0.0.1<\/code> and <code>0.0.0.0<\/code> are completely different exposure stories.<\/li>\n<li><strong>Containers, socket activation and <code>TIME_WAIT<\/code><\/strong> explain nearly every case where the port appears to belong to nobody.<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>A service will not start because &#8220;Address already in use&#8221;. A port is open that nobody can account for. Both questions come down to the same thing: which process owns this listening socket? Here is how to answer it with ss, lsof, fuser and \/proc \u2014 plus what to do when the port appears to belong to nobody at all.<\/p>\n","protected":false},"author":1,"featured_media":198,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3,4,8],"tags":[],"class_list":["post-196","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-linux","category-networking","category-tutorials"],"_links":{"self":[{"href":"https:\/\/backendside.com\/blog\/wp-json\/wp\/v2\/posts\/196","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/backendside.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/backendside.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/backendside.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/backendside.com\/blog\/wp-json\/wp\/v2\/comments?post=196"}],"version-history":[{"count":1,"href":"https:\/\/backendside.com\/blog\/wp-json\/wp\/v2\/posts\/196\/revisions"}],"predecessor-version":[{"id":199,"href":"https:\/\/backendside.com\/blog\/wp-json\/wp\/v2\/posts\/196\/revisions\/199"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/backendside.com\/blog\/wp-json\/wp\/v2\/media\/198"}],"wp:attachment":[{"href":"https:\/\/backendside.com\/blog\/wp-json\/wp\/v2\/media?parent=196"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/backendside.com\/blog\/wp-json\/wp\/v2\/categories?post=196"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/backendside.com\/blog\/wp-json\/wp\/v2\/tags?post=196"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}