Two Apps, One Port

Someone pinged me with the kind of bug that makes your skin crawl: “I save a note, it saves fine, no error. But when I come back later it’s gone. And sometimes I open a file and it’s the wrong file entirely.”

No errors. No crash. Data just… wrong. The worst kind of ticket — because “wrong data, no error” means the system is lying to you with a straight face.

I assumed corruption. Maybe a bad sync, a stale cache, a half-written file on disk. I was wrong about all of it.

The investigation

First thing I did was stop trusting the app and start trusting the wire. The app talked to itself over a local REST API on a fixed port. So I asked the only question that matters when data is “wrong”:

Am I even talking to the thing I think I’m talking to?

The user ran two separate profiles of the same application — two distinct workspaces, two configs, two processes. Different data, different windows. But same binary, same defaults.

I checked what was actually listening.

        Profile A (started first)        Profile B (started second)
        ┌──────────────────────┐         ┌──────────────────────┐
        │  app instance A      │         │  app instance B      │
        │  workspace: WORK     │         │  workspace: PERSONAL │
        └──────────┬───────────┘         └──────────┬───────────┘
                   │ bind :8200 OK                  │ bind :8200 FAIL
                   ▼                                ▼
            ┌─────────────┐                  (silent: port taken)
            │  TCP :8200  │ ◄──── requests land HERE
            └─────────────┘        regardless of which UI you used
            edits in B's UI → API call → answered by A

Both instances were configured to expose their REST API on the same hard-coded port. Whichever process won the startup race grabbed the socket. The loser tried to bind, failed, and — because the failure was non-fatal — just shrugged and kept running its UI as if nothing happened.

So you’d type into Profile B’s window. The UI would fire an API write. That write went to port 8200, which Profile A owned. Profile A happily saved it into the wrong workspace. Reads came back from A too. Profile B’s UI was a puppet with cut strings.

The “aha”

The tell was netstat. One PID owned the port. Not two. Not a conflict logged anywhere — just one quiet winner.

# Windows: who actually holds the port?
netstat -ano | findstr :8200
#   TCP    127.0.0.1:8200    0.0.0.0:0    LISTENING    18044

tasklist /fi "PID eq 18044"
#   app.exe    18044   ...   <- this is Profile A, not B
# Linux/macOS equivalent
lsof -nP -iTCP:8200 -sTCP:LISTEN
ss -ltnp 'sport = :8200'

One listener. Two apps. The data wasn’t corrupt — it was just going to the wrong house, and the mailman never complained.

Profile Aworkspace: WORKProfile Bworkspace: PERSONAL:8200one listenerbind OKbind FAIL → routes to A
Both UIs wired to one fixed port. The first process to start owns it; the second silently misroutes.

The fix

Stop them from racing for the same socket. Give each instance its own port.

# 1. Edit each profile's config — distinct ports per instance
#    Profile A
"api": { "enabled": true, "port": 8200 }
#    Profile B
"api": { "enabled": true, "port": 8201 }

# 2. Fully stop both instances (no orphan holding the old socket)
taskkill /f /im app.exe        # Windows
# pkill -f app                 # Linux/macOS

# 3. Confirm the ports are actually free before restart
netstat -ano | findstr "8200 8201"   # expect: nothing

# 4. Start both, then verify ownership — two PIDs, two ports
netstat -ano | findstr "8200 8201"
#   TCP  127.0.0.1:8200  LISTENING  20110   <- Profile A
#   TCP  127.0.0.1:8201  LISTENING  20338   <- Profile B

Two listeners, two PIDs, two ports. Writes to B stayed in B. Reads told the truth. The “disappearing edits” were gone because they’d never disappeared — they’d just been filed in the wrong cabinet.

Why it happened

Defaults. The app shipped with a single hard-coded API port and assumed exactly one instance would ever run. Reasonable for the common case, fatal for power users running multiple profiles. There was no port-conflict warning because a failed bind on a convenience API isn’t fatal — the app degrades silently instead of refusing to start. Silent degradation is how a config bug masquerades as data loss.

Takeaways

  • When data looks “wrong” but nothing errors, question the endpoint first. Confirm you’re talking to the instance you think you are before you go disk-spelunking for corruption.
  • netstat -ano / lsof -i is ground truth for “who owns this port.” One listener where you expected two is the whole bug, right there.
  • Multiple instances of the same app will knife-fight over hard-coded ports. Give every instance a unique port in its config — don’t trust defaults to coexist.
  • A non-fatal bind failure is a trap. Silent degradation turns a 5-minute config fix into a week of phantom data-loss tickets.
  • Pre-flight before restart: kill all instances, verify the port is actually free, then start — and read back the listeners to prove it stuck.