← Field Notes
Published Last updated 15 min read

What an Unresolved Satisfactory Connection Taught Me

Why a game server that worked locally but failed remotely became a better lesson in evidence, network boundaries, and honest troubleshooting than a guessed fix ever could.

In this article
  1. The evidence ledger from the incident
  2. ”Works locally” is useful evidence, but narrow evidence
  3. Seeing a connection attempt is not the same as completing a session
  4. The useful troubleshooting model became a set of boundaries
  5. I needed a network contract before I needed another setting
  6. What I would capture before changing anything now
  7. External testing has to be genuinely external
  8. The stop-guessing boundary
  9. Changing one thing at a time is slower until it is faster
  10. An unresolved incident can still produce finished documentation
  11. The incident improved the next deployment more than a lucky fix would have
  12. What remains unresolved
  13. Related field notes
  14. Security note
  15. AI transparency

The Satisfactory server worked on the local network.

Remote players still could not complete the connection.

That sounds like the beginning of a troubleshooting guide with a satisfying final section: one hidden setting, one corrected rule, one restart, problem solved.

That is not what happened.

I never reached a root cause I could prove strongly enough to publish as fact. I had plausible explanations, useful tests, and several things I could rule out, but I did not have the clean before-and-after evidence that would let me say, “This was the problem, and this change fixed it.”

For a while, that made the incident feel incomplete.

Now I think the unresolved ending is the most useful part of it.

The server forced me to separate reachability from usability, observation from explanation, and a plausible fix from a defensible conclusion. It also changed the way I document network problems in the homelab. Instead of treating uncertainty as something to edit out later, I try to preserve it until the evidence actually closes the gap.

The evidence ledger from the incident

The original troubleshooting session did not leave me with a complete packet capture, a synchronized client/server trace, or one final configuration change followed by a verified remote join. I am not going to recreate that evidence after the fact.

What I can preserve is the boundary between what the incident did prove, what it did not prove, and what remained unknown.

Observation or claimEvidence stateWhat I can responsibly conclude
A client on the local network could join and load the worldObservedThe server could provide a valid session over at least one client path
The server process remained running during the testsObservedA total process crash was not the explanation for every failed remote attempt
Server-side activity lined up with some remote connection attemptsObservedAt least some traffic or session activity associated with the remote attempt reached a server-visible layer
A remote player could not complete the joinObservedThe real external user path remained broken
The public-facing network path was fully correctNot provenSeeing an attempt at the server was not enough to prove every required direction, protocol, translation, or reply path
The host firewall was completely uninvolvedNot provenOne successful local path and partial remote visibility could not exclude a later firewall boundary
The container or management layer published every required socket correctlyNot provenThe historical evidence was not strong enough to close that boundary
One specific forwarding rule, port, protocol, or game setting caused the failureUnknownI do not have a controlled before-and-after result that justifies naming one root cause

That table is the finished state of the incident.

It is less satisfying than a magic fix. It is more useful than a false one.

”Works locally” is useful evidence, but narrow evidence

A local client could join the server and load the world.

That mattered. It established that the game process could start, its data was readable, and at least one client path could reach the application successfully. It also made a completely broken installation less likely.

But I initially gave that result more weight than it deserved.

Local success did not prove that an external client could traverse every network boundary required for a complete session. It did not prove that the gateway, host firewall, container networking, management layer, and application were all expressing the same network contract. It did not prove that replies followed the path I assumed they followed.

The lesson was simple:

A successful test proves the path that was tested, not every path that looks similar on a diagram.

That sounds obvious written down. During an incident, it is surprisingly easy to forget.

A local connection feels close to success. The service is alive. The world loads. The server console looks normal. The remaining problem starts to feel like one little edge rule that should be easy to find.

That feeling is not evidence.

In What My Homelab Taught Me About Calm Troubleshooting, I wrote about reducing uncertainty instead of chasing the first explanation that feels right. This incident is one of the reasons I think that way now. “Local works” was not the answer. It was one boundary successfully crossed.

Seeing a connection attempt is not the same as completing a session

The server logs showed activity that lined up with remote connection attempts.

That was the most tempting clue in the entire incident.

If the server saw something when the remote player tried to join, then traffic had clearly made it through at least part of the path. It was easy to turn that into a much stronger conclusion: the forwarding must be correct, the firewall must be fine, and the remaining problem must be inside the game server.

The client still failed.

What the log actually proved was much smaller. Some traffic associated with the attempt reached a point where the server could record it.

It did not prove that the entire exchange completed. It did not prove that every required direction, protocol, socket, or translated path behaved correctly. It did not prove that the client received the replies it needed. It did not prove that another layer was not allowing the first part of a session and interfering with a later part.

A connection is a sequence, not a single packet-shaped event.

That changed how I read logs. I stopped asking whether a log entry looked encouraging and started asking exactly where in the transaction the log entry belonged.

Did the application receive an initial request?

Did it create a session?

Did the client progress beyond discovery or negotiation?

Did the server report a disconnect reason?

Did the event prove application success, or only network arrival?

The narrower I made the question, the less tempted I was to promote one log line into a diagnosis.

The useful troubleshooting model became a set of boundaries

Once I stopped treating the problem as “the server is not reachable,” the investigation became easier to structure.

There were several boundaries to test independently:

  1. Application state. Is the server process running, stable, and listening where it is expected to listen?
  2. Host state. Is the operating system exposing the expected sockets and allowing the traffic the application needs?
  3. Container or service-management state. If another layer publishes or maps the service, does its configuration agree with the host and application?
  4. Gateway state. Is the edge device translating and forwarding the intended traffic to the intended destination?
  5. External path. Does a client from a genuinely separate network reach the service in the way a real remote player would?
  6. Application session. Does the game actually complete the connection, not merely receive an initial attempt?

This was far more useful than repeatedly checking one forwarding screen.

The boundaries also prevented a common troubleshooting mistake: using one test to answer a question it was never designed to answer.

A local join can test local application usability.

A socket check can show whether something is listening.

A packet or log observation can show that traffic reached a layer.

A generic reachability test can answer a limited network question.

Only a successful real client session proves that the real client session works.

That distinction now shapes the test matrices I use for other services too. In The Test Matrix I Use Before I Trust a Change, I describe validation as a set of expected outcomes across meaningful paths. This incident was a good example of why one green check is rarely enough.

I needed a network contract before I needed another setting

The biggest process change came afterward.

For any hosted service, I now want a small network contract before I start changing rules.

Not a full network diagram. Not pages of documentation. Just enough information to make the expected path explicit.

For a game server, that record might include:

  • which application or service is supposed to receive the connection;
  • which transports the current application documentation requires;
  • which interfaces or network scopes the service should listen on;
  • whether a container or management platform publishes the service;
  • which firewall layers can allow or deny it;
  • which gateway translation is expected;
  • which client path counts as a real external test;
  • what a successful session looks like from both client and server perspectives.

The important word there is current.

Game-server networking requirements can change across releases and server implementations, so I do not preserve old port numbers or protocol assumptions as timeless truth. Before a new deployment or repair, I verify the current requirements against the official documentation for the software actually being used.

That keeps the troubleshooting record focused on the method instead of turning an old incident into an accidental configuration guide.

The contract also gives each layer something concrete to agree with. If the application expects one thing, the host exposes another, and the gateway forwards a third, the problem is no longer “remote players cannot join.” It is a mismatch I can name and test.

What I would capture before changing anything now

The missing artifact from the historical incident was a synchronized snapshot of the full path before I began moving settings around.

For the next game-server connection problem, I would capture a small evidence packet first.

On the host, that would include the current time and listening state:

date -Is
sudo ss -lntup

If the server is containerized, I would preserve the running container state and effective mappings rather than relying only on the configuration I intended to deploy:

docker ps --format 'table {{.Names}}\t{{.Ports}}\t{{.Status}}'
docker inspect <server-container> > server-container-inspect.json

For the host firewall, I would capture the active system rather than assuming which frontend owns the rules:

sudo ufw status verbose
sudo nft list ruleset

I would also save privately:

  • the current game-server network requirements from the software’s official documentation;
  • the relevant management-panel allocation or publication settings;
  • the gateway forwarding/NAT rule as actually configured;
  • the server log covering a known test window;
  • the external client’s timestamp and visible error/result;
  • the result of one local join under the same server state.

The exact addresses, ports, account identifiers, and management URLs remain private. The public lesson does not require publishing the deployment.

The value of the packet is correlation. If the external client attempts a join at 10:14:22, I want the server, host, gateway, and client evidence to be close enough in time that I can compare the same event instead of vaguely matching logs from different tests.

External testing has to be genuinely external

Another lesson was that the location of the test client matters.

Testing a public-looking address from inside the same network can behave differently from testing it from a separate network. The exact behavior depends on the network equipment and configuration, so I do not treat an internal test as a substitute for the real client path.

For this incident, I needed a client outside the local network because that was the path that was failing.

That seems trivial, but it has a larger operational consequence: the test environment is part of the evidence.

When I write down a result now, I try to preserve enough context to know where it came from.

“Connection failed” is weak documentation.

“External client failed while local client succeeded under the same server state” is much stronger.

The second statement gives me a boundary. It narrows where the difference can live.

That same pattern applies far beyond game servers. A service might work from the host but not another machine, from one VLAN but not another, through the local address but not the reverse proxy, or from an administrator account but not a normal user. Each difference is information if I resist flattening all of them into “it works” or “it is broken.”

The stop-guessing boundary

There is a point where another speculative setting change stops being troubleshooting.

For this class of incident, I would stop changing configuration and switch to evidence capture when all of these are true:

  • the server process is stable;
  • a local client can complete a real session;
  • the expected server sockets are listening;
  • the effective container or management-layer publication has been recorded;
  • the current firewall and gateway rules have been captured;
  • a genuinely external client still fails;
  • server-side logs or packet evidence show at least some activity associated with that external attempt;
  • no single change has yet produced a controlled successful remote retest.

At that point the useful question is no longer, “What else can I toggle?”

It is, “Where does the failed transaction stop?”

A targeted packet capture can help answer that without pretending packet arrival is the same as application success. The filter should be built from the current documented transport and the specific test, not copied from an old blog post.

A generic shape might be:

sudo tcpdump -ni any '<narrow filter for the current documented game traffic>'

The purpose is to correlate the client attempt with what reaches and leaves the host. Packet capture still does not prove that the application accepted the session; it gives another boundary to compare with application logs and client behavior.

If I cannot capture enough evidence to identify the failing boundary, the correct state remains unresolved.

Changing one thing at a time is slower until it is faster

Once an incident has lasted long enough, changing several plausible settings at once becomes extremely attractive.

Add another rule. Change a binding. Restart the service. Change the container mapping. Rebuild the forwarding rule. Disable a firewall temporarily. Try again.

Sometimes that produces a working system.

It also destroys the explanation.

If five things change and the next test succeeds, I know only that the final configuration worked once. I do not know which change mattered, which changes were unnecessary, whether one change weakened security, or whether the result will survive the next restart.

For this server, I did not have a clean final change followed by a verified remote join. That is precisely why I will not invent one for the sake of a better ending.

The better process is boring:

  • preserve the current state;
  • state the hypothesis;
  • change one relevant variable;
  • repeat the same external test;
  • record the result;
  • either keep the change because the evidence supports it or revert it and continue.

This is slower than random movement for the first few minutes.

It is much faster when the problem becomes complicated enough that memory stops being reliable.

An unresolved incident can still produce finished documentation

I used to think a troubleshooting note needed a root cause before it was worth publishing.

Now I think it needs an honest boundary.

There is a major difference between an unfinished investigation and a finished account of an unresolved investigation.

An unfinished note says, “I tried some things and it still did not work.”

A useful unresolved retrospective says:

  • what behavior was observed;
  • which paths succeeded;
  • which paths failed;
  • what each test proved;
  • which explanations became less likely;
  • which explanations remained plausible;
  • what evidence was missing;
  • what process changed because of the incident.

That is enough to teach something without pretending to know more than I know.

It is also safer technical writing.

A guessed root cause can send another person into their firewall, router, container stack, or operating system making changes that have nothing to do with their failure. Preserving uncertainty is not weakness. It is part of keeping the advice proportional to the evidence.

That is the same standard I use in The Mistakes I Record After Every Project. A retrospective becomes useful when it records the assumption, signal, test, result, and resulting guardrail instead of rewriting the past into a clean success story.

The incident improved the next deployment more than a lucky fix would have

The Satisfactory server did not give me the satisfying ending I wanted.

It gave me a better checklist.

Now, before exposing a service externally, I am more likely to define the network contract first, record the expected listening state, identify every policy layer in the path, establish a real external test, capture timestamps early, and decide what success looks like before I start changing things.

That process scales better than remembering the magical setting that fixed one old server.

A one-time fix teaches me how to repair one configuration.

A better troubleshooting model changes how I approach the next ten.

That is the larger value of a homelab for me. The services are useful, but the failures are where the operating habits get tested. When the environment is mine, I get to follow the problem across application, operating system, container, firewall, gateway, and client boundaries instead of seeing only one slice of the incident.

The goal is not to eliminate failure.

It is to make the next failure easier to reason about.

What remains unresolved

I still do not have a single confirmed root cause for that historical remote-connection failure.

I do not have a final change followed by a controlled, verified remote join that would justify naming one setting as the fix.

That is the honest state of the incident, and I am leaving it that way.

What I do have is a stronger rule for future troubleshooting:

Every test gets a narrow meaning. Every change gets a hypothesis. Every conclusion has to fit the evidence that actually exists.

That is more useful to me than a guessed victory lap.

Security note

Exact addresses, historical port mappings, hostnames, usernames, domains, player identifiers, management URLs, container names, internal paths, and other environment-specific details are intentionally omitted or generalized. No credential material or account identifiers are included.

AI transparency

AI assisted with organizing, editing, and designing the evidence-ledger format for this revision. The local and remote behavior, troubleshooting uncertainty, and conclusions are based on my own homelab troubleshooting work. The September 14, 2026 revision does not claim a historical root cause that the original evidence did not prove.

JO

Written by

Jessie Owens

I run Eldritch IT and write about the systems, repairs, infrastructure decisions, and business lessons behind the work.