Skip to content
Documentation menu

Watch & Why

CLI

Probe a failed bind or stream socket changes without terminating processes.

why combines one process snapshot with a short-lived bind probe. watch compares successive native snapshots. Neither command sends signals or invokes Docker.

why • the real reason a bind fails

Address already in use is a symptom with at least four different causes. why separates them by taking one snapshot, then attempting the exact bind and closing it immediately.

kick why 3000
$ kick why 3000WHY port=3000 protocols=TCP addresses=127.0.0.1,::1 snapshot=partialscope kind=current_network_namespace identifier=net:[4026531833]tcp://127.0.0.1:3000 verdict=owned certainty=proven label=web dev  probe=address_in_use raw_os_error=98  evidence code=visible_verified_owner source=linux_procfs    PID 18422 (node) owned the endpoint at snapshot capture  evidence code=exact_bind_address_in_use source=bind_probe    the exact bind probe reported that the address is in usetcp://[::1]:3000 verdict=bindable_now certainty=proven label=-  evidence code=exact_bind_succeeded source=bind_probeaggregate_exit_code=3

Note the two rows. 127.0.0.1:3000 is taken; [::1]:3000 is free. A tool that reports “port 3000 is busy” hides exactly that distinction.

Which endpoints get probed

The default query is TCP on 127.0.0.1, then ::1 • the two addresses a local dev server usually means. Widen it explicitly:

why examples
kick why 3000
kick why 5353 --udp --address 127.0.0.1
kick why 3000 --tcp --address :: --ipv6-only
kick why 3000 --all-protocols --all-addresses --json
  • --tcp / --udp / --all-protocols • protocol selection. TCP is ordered before UDP.
  • --address ADDR • one literal local IP. No %zone syntax • use --scope-id instead.
  • --all-addresses127.0.0.1, 0.0.0.0, ::1, then ::. It does not enumerate your interfaces.
  • --scope-id ID • a nonzero IPv6 interface index; requires one explicit IPv6 --address.
  • --ipv6-only / --dual-stack • mutually exclusive, and valid only when every address is IPv6. With neither, you get the OS default.
  • --reuse-address • probe with address reuse enabled.

The matrix is protocol-major and capped at eight endpoints.

Verdicts and certainty

Every endpoint gets a verdict and a certainty that belongs to that specific claim:

Certainty Means
proven Directly observed • a probe result or a verified owner.
estimated Derived from a bounded calculation, such as a Linux TCP timer.
heuristic Inferred from a pattern rather than a fact.
unknown Not established • usually a scope or permission limit.

Verdicts distinguish bindable, occupied, permission denied, address unavailable, unsupported behaviour, and other retained OS errors. Each one carries ordered evidence lines and, where something couldn’t be established, gap lines naming what’s missing.

Exit codes

why uses the shared exit-code contract with one extra rule: for a multi-endpoint query, the aggregate is the worst result present. Precedence is 1, then 4, then 3, and 0 only when every endpoint proved bindable.

watch • ports changing hands

watch polls full-state snapshots and emits an event whenever something changes.

kick watch --tcp --port 3000
$ kick watch --tcp --port 3000BIND TCP 127.0.0.1:3000 listen owners=18422 label=web dev filter=matched certainty=provenRELEASE TCP 127.0.0.1:3000 listen owners=18422 label=web dev filter=matched certainty=proven
watch examples
kick watch
kick watch --tcp --address 127.0.0.1 --port 3000
kick watch --filter label:web --interval 500ms
kick watch --filter state:established --duration 30s --json

Events

Event Emitted when
baseline The first valid snapshot, describing what was already open.
bind An endpoint appeared.
release An endpoint disappeared.
replacement The same endpoint is now held by a different owner identity.
collection_gap A poll failed. The last valid snapshot is kept rather than discarded.

replacement exists because a PID alone isn’t identity. Kickoutchi compares PID plus a start marker, so a recycled PID on the same port is reported as a replacement instead of silently looking like nothing happened.

Timing

  • --interval accepts 100ms..=60s; the default is 1s.
  • --duration accepts 100ms..=7d. Without it, watch runs until Ctrl-C.
  • Polls never overlap. If collection is slower than your interval, the next poll waits rather than building a backlog.

Failure and recovery

Collection can fail transiently. watch is explicit about it rather than fabricating events:

  • A failed initial collection emits nothing and exits 1.
  • After a valid baseline, a failed poll emits collection_gap, keeps the last valid snapshot, and retries. Recovery compares against that retained snapshot, so a transient failure can’t manufacture a wave of false release events.
  • The third consecutive failure flushes its gap and exits 1.
  • Collection gaps bypass endpoint filters • you always learn that you stopped seeing.

Ctrl-C, duration expiry, and a closed stdout consumer (kick watch --json | head) all exit successfully.

Filtering

watch accepts the shared filter grammar plus a watch-only state: field covering every native TCP state:

state: values
listen bound closed syn_sent syn_received established fin_wait1 fin_wait2
close_wait closing last_ack time_wait delete_tcb new_syn_received unknown

When a filter depends on a fact Kickoutchi couldn’t read • an owner PID, a process name • the event is emitted with filter=indeterminate rather than being dropped. A definite non-match is still suppressed. You are told when a filter might have hidden something.

Next

Both commands have machine-readable output. See Structured output for the kickoutchi.watch_event/1 and kickoutchi.why/1 schemas.