26 August 2026

Orphaned Processes and Port Conflicts: Finding, Killing, and Preventing Them

You start an app, something goes wrong, you hit Ctrl+C — and now the app won't start again because the port is already in use. Sound familiar? This is the orphaned process problem, and it's more common than it should be.

What Happened

I was running JiuwenClaw, a Python-based AI agent platform. It launches several sub-processes on startup: an agent server and a gateway, each binding to their own port. After terminating the parent with Ctrl+C, the app refused to restart:

Port 19001 already in use
Port 18092 already in use

Why Ctrl+C Doesn't Always Clean Up

When you press Ctrl+C in a terminal, the shell sends SIGINT to the foreground process group. If the parent process spawned children via Python's subprocess module without explicitly adding them to the same process group — or without a signal handler that forwards the signal — those children keep running after the parent dies.

They become orphans: no parent, no controlling terminal, but still holding their network ports open.

Finding the Culprits

Two commands are all you need.

Check which ports are in use:

ss -tlnp | grep -E '19001|18092'

Output:

LISTEN  127.0.0.1:19001  users:(("python3",pid=80615,...))
LISTEN  127.0.0.1:18092  users:(("python3",pid=80613,...))

Identify the processes by PID:

lsof -i :19001 -i :18092

Output:

python3  80613  user  ...  TCP  localhost:18092 (LISTEN)
python3  80615  user  ...  TCP  localhost:19001 (LISTEN)

Confirm what they are:

ps -p 80613 -o pid,ppid,cmd --no-headers
ps -p 80615 -o pid,ppid,cmd --no-headers

Output:

80613  2602  python3 -m jiuwenclaw.app_agentserver
80615  2602  python3 -m jiuwenclaw.app_gateway

There they are — orphaned sub-processes from the previous run, still alive and holding the ports.

Killing Them

Once you have the PIDs:

kill 80613 80615

Verify the ports are clear:

ss -tlnp | grep -E '19001|18092'

No output means the ports are free. You can now restart the app normally.

If a process ignores SIGTERM, escalate with SIGKILL:

kill -9 80613 80615

One-liner: kill by port

If you just want to nuke whatever is on a port without finding the PID first:

fuser -k 19001/tcp 18092/tcp

Or using ss and kill together:

ss -tlnp | grep 19001 | grep -oP 'pid=\K[0-9]+' | xargs kill

Mitigations

1. Fix the application's signal handling (ideal)

The root cause is that the app doesn't forward signals to its children. A well-behaved Python launcher should use a process group and kill the whole group on exit:

import os
import signal
import subprocess

proc = subprocess.Popen(["python3", "-m", "myapp.server"], start_new_session=True)

def cleanup(sig, frame):
    os.killpg(os.getpgid(proc.pid), signal.SIGTERM)

signal.signal(signal.SIGINT, cleanup)
signal.signal(signal.SIGTERM, cleanup)

Using start_new_session=True puts the child in its own process group, and os.killpg kills the whole group cleanly.

2. Use a stop script or PID file

Many services write a PID file on startup and provide a stop command. If the app supports it, prefer jiuwenclaw-stop over Ctrl+C — a proper stop command can clean up child processes before exiting.

3. Wrap the launcher in a shell trap

If you're starting the app via a shell script, add a trap:

#!/bin/bash
jiuwenclaw-start &
PARENT_PID=$!

cleanup() {
    kill -- -$(ps -o pgid= $PARENT_PID | tr -d ' ')
}

trap cleanup EXIT INT TERM
wait $PARENT_PID

The kill -- -<pgid> sends the signal to the entire process group.

4. Run inside a process supervisor

Tools like systemd, supervisord, or s6 track all child processes and kill them as a unit when the service stops. If you're running something in production or semi-permanent, this is the right answer — it also handles restarts, logging, and health checks.

5. Check before you start

Add a pre-flight check to your workflow. Before starting any service, quickly verify the ports it needs are free:

ss -tlnp | grep -E '19001|18092' && echo "Port conflict!" || echo "Ports clear"

Summary

Step Command
Find listening ports ss -tlnp \| grep <port>
Find PID by port lsof -i :<port>
Confirm process identity ps -p <pid> -o pid,cmd
Kill by PID kill <pid>
Kill by port directly fuser -k <port>/tcp
Verify ports are free ss -tlnp \| grep <port>

The quick fix is always kill. The real fix is making sure the app cleans up after itself — or wrapping it in something that does.

No comments:

Post a Comment