Building a Multithreaded Chess Server in C: TCP, Threads, Pipes, and Stockfish
Building a Multithreaded Chess Server in C: TCP, Threads, Pipes, and Stockfish
A chess server is a surprisingly good systems-programming project.
At first glance, the application sounds simple:
connect a clientreceive a movecheck the movesend a responseThe interesting part appears when the requirements become more realistic.
Several clients may be connected at the same time. Some want to play the computer, while others want to be paired with another human. The chess engine is a separate process. Network communication is asynchronous. A human opponent can disconnect halfway through a game. The server has to retain game state, serialize access to shared resources, and continue running even when one connection disappears.
That turns a chess application into a compact exercise in:
TCP networkingPOSIX threadsprocess creationpipes and file descriptorsinter-process communicationmutual exclusionprotocol parsingshared statefailure handlingThe main lesson I took from this project was not about chess.
It was about where state should live when several independently executing parts of a program need to cooperate.

Multiple TCP sessions are handled concurrently, while game state and the single chess-engine process are shared resources.
The Architecture Has Three Different Kinds of Concurrency
I found it useful to separate the problem into three concurrency domains.
1. Client-side asynchrony
The client has two independent input sources:
stdinnetwork socketA user may type a command while a server message is arriving.
2. Server-side client concurrency
Every connected client has an independent network session.
The server therefore needs to make progress on many sockets at the same time.
3. Process-level concurrency
The chess engine is not a library call.
Stockfish is a separate process with its own stdin and stdout, connected to the server with pipes.
These look similar because all three involve “multiple things happening”, but they require different abstractions.
That distinction helped me avoid treating every problem as simply another thread.
Lesson 1 - A Network Client Has to Listen in Two Directions
A blocking command-line client is easy to imagine:
read user inputsend requestwait for replyrepeatThat model assumes every message from the server is a direct reply to the most recent request.
A multiplayer game breaks that assumption.
If another human player makes a move, the server can send a message even though the local user did not just type anything. A game can also end because the other player resigns or disconnects.
So the client really has two independent flows.

One path reads commands from stdin.
The other continuously reads messages from the TCP connection.
The client itself should remain relatively thin. It does not need to understand the whole board. It only needs enough local state to decide whether commands such as move, hint, or resign are currently meaningful.
For example:
game in progress?my turn?my colour?The authoritative chess position belongs on the server side.
This gave me a useful distributed-systems rule:
A client should store only the state it needs to provide its interface. The authoritative state should live where decisions are validated.
Lesson 2 - Text Protocols Are Excellent for Debugging
The client and server communicate over TCP using a line-oriented text protocol.
Typical messages can be thought of as:
start computer whitestart human eithermove e2e4hint besthint allboardresignand responses as:
started whiteokmoved e7e5checkmoves ...gameover ...For a student project, a text protocol has a major advantage: it is observable.
I can connect with a simple terminal tool and manually send commands.
That lets me test the server independently from my own client implementation.
This is an underrated design property.
When both sides of a protocol are being developed at the same time, a human-readable wire format makes it much easier to answer:
Is the problem in the client, the server, or the protocol?
Binary protocols may be more compact, but debuggability has real engineering value.
Lesson 3 - One Thread Per Client Is Simple Until State Becomes Shared
The server accepts incoming TCP connections and creates one handler thread for each client.
That is conceptually straightforward:
accept connection ↓spawn handler thread ↓read commandprocess commandsend responserepeatThe difficulty appears when threads stop being independent.
Two client threads may refer to the same human-vs-human game.
Many client threads may need the same Stockfish process.
Several threads may access the waiting-player list.
At that point, the important question is no longer:
How many threads do I have?
It becomes:
Which data can be touched by more than one thread?

Every shared object needs a clearly defined synchronization policy.
A large mutex around everything may be correct but unnecessarily restrictive.
No mutex at all may work in simple tests and fail unpredictably under simultaneous connections.
The architecture gets much easier to reason about when shared resources are listed explicitly:
connected-client registryhuman-matchmaking poolshared Game objectsStockfish stdin/stdout transactionLesson 4 - A Single Chess Engine Becomes a Serialized Service
One of the most interesting constraints was that the server uses one Stockfish process even while supporting multiple clients and games.
That means the engine cannot safely be treated as if every client thread owns its own private chess library.
Suppose two threads do this at the same time:
Thread A:position <game A>go ...
Thread B:position <game B>go ...If those command sequences interleave, the response no longer has an unambiguous owner.
The engine interaction therefore has to be treated as a transaction:
lock engineset game contextsend UCI commandread complete responseunlock engineThe engine lock is not merely protecting a file descriptor.
It protects the semantic integrity of a request-response conversation.
That distinction matters.
Many concurrency bugs happen because programmers lock individual writes but forget that a protocol operation consists of several ordered writes and reads.
Lesson 5 - Pipes Turn a Program into a Service
Stockfish runs as a child process.
The server creates two pipes:
server -> Stockfish stdinStockfish stdout -> serverthen forks, redirects file descriptors in the child, and launches the engine.

The startup sequence also has a small protocol of its own.
Conceptually:
start process ↓isready ↓readyok ↓uci ↓uciokThis was one of the parts of the project I liked most because it connects several Unix concepts that can otherwise feel unrelated:
pipe()fork()dup / descriptor redirectionexec()FILE* or descriptor I/Oprocess lifecycleThe result is effectively a local service boundary.
Stockfish could have been a library, but using a process gives useful isolation:
- the engine has its own address space;
- it can be replaced independently;
- communication is constrained to a documented protocol;
- engine termination can be detected through the pipe.
Lesson 6 - FEN Is a Better Game-State Boundary Than a Thread
A common mistake in threaded applications is to associate state too strongly with the thread currently executing.
A thread is only an execution context.
The chess game itself has a longer lifetime.
A useful game object needs to know things such as:
in progress or finishedwhite playerblack playercurrent / final board statewhose turn is nextThe board state can be represented using FEN (Forsyth-Edwards Notation).

This creates a useful separation:
Client = communication endpoint
Thread = code currently handling the endpoint
Game = shared application state
FEN = serializable chess positionFor a human-vs-human game, two client threads can point at the same Game.
Neither thread is the game.
That lesson generalizes well beyond chess.
In concurrent servers, long-lived domain state should usually be represented explicitly rather than hidden inside thread-local control flow.
Lesson 7 - The Engine Can Be Reconstructed from State
Because the server stores the board as FEN, the single Stockfish instance can be switched between games.
Before asking the engine a question, the server can reconstruct the relevant position:
position fen <current FEN>and then issue a query.
This is what makes sharing one engine practical.
The engine is not trusted as the only copy of the application state.
Instead:
server game state ↓reconstruct Stockfish context ↓ask question ↓parse response ↓update server game stateThat architecture is much safer than allowing the child process to become an undocumented global state store.
It also resembles how stateless backend services are designed: send enough context with each transaction that the service can answer correctly.
Lesson 8 - Move Validation Is a Pipeline, Not a Boolean Function
A move such as:
e2e4looks like it should produce:
valid / invalidBut the full server operation is richer.

A useful conceptual sequence is:
1. load the current FEN2. apply the proposed move in the engine context3. inspect the resulting board4. determine whether the state changed5. count legal replies6. determine check / checkmate / stalemate7. if playing the computer, request its best response8. update FEN9. notify the relevant client or clientsThe UCI interface gives several tools for different parts of that process.
position
Defines the board state and optionally applies a candidate move.
d
Returns diagnostic board information, including state that can be used to recover the current FEN and detect check conditions.
go perft 1
Enumerates legal moves from the current position.
The number of legal replies matters when distinguishing:
checkcheckmatestalematego movetime ... depth ...
Searches for a best move, which can be used for hints or the computer opponent.
This part taught me that “validation” in a real application often crosses several subsystems.
It is not necessarily one function call.
Lesson 9 - Matchmaking Is a Shared-State Problem
Computer games are comparatively easy because one client owns one game.
Human matchmaking introduces a waiting pool.
A player can request:
whiteblackeitherThe server has to find a compatible waiting client and create one shared game.

This means matchmaking requires a data structure that exists outside any single client thread.
Conceptually:
Client A asks for white ↓search waiting pool ↓compatible black/either player? ↓yes -> create game and notify bothno -> record Client A as waitingThe subtle part is that another client thread may modify the same pool at the same time.
So matchmaking is simultaneously:
application logic+concurrency controlI found this to be a good example of why race conditions are often really domain-state races, not just low-level memory races.
Lesson 10 - Disconnects Are Part of the Game Protocol
A socket closing is a transport event.
In a multiplayer game, it also has application meaning.
If one human player disconnects during a game, the other player needs to be told that the game is over.
So cleanup is not simply:
close(fd);return;The server may need to:
mark the game finishednotify the opponentremove the client from matchmakingrelease client resourcesclose the socketterminate only that handler thread
This is another systems lesson that I found useful:
Resource cleanup and application-state cleanup are not always the same thing.
Closing the file descriptor handles the operating-system resource.
It does not automatically repair the application state that referenced that connection.
Lesson 11 - SIGPIPE Is a Networking Design Issue, Not an Edge Case
Writing to a closed pipe or socket can generate SIGPIPE.
If the default action is allowed to terminate the process, one disconnected client could accidentally kill the whole server.
Similarly, failure of the Stockfish pipe means something very different from failure of one client connection.
The failure policy should reflect the ownership boundary:
client socket failure -> terminate one client session -> server continues
Stockfish process failure -> shared engine unavailable -> all games lose engine service -> notify clients and terminate serverThis is a clean example of failure domains.
The scope of recovery should match the scope of the failed resource.
Lesson 12 - Blocking Is Good When There Is Nothing to Do
Concurrent programs sometimes become complicated because developers try too hard to make everything continuously active.
A server thread waiting for work should often simply block.
Examples include:
accept()read()fgets()getline()A blocked thread consumes almost no CPU while waiting for an event.
The bad alternative is:
check socketnothing therecheck againcheck againsleep a littlecheck againBusy waiting adds latency, wastes CPU, and makes the server harder to reason about.
This project reinforced an important Unix programming habit:
If progress depends on external input, block on the object that will deliver that input.
A Data Model I Would Use
If I were structuring the project again, I would make the major domain objects explicit.
Client
socket / FILE streamsselected game moderequested colourcurrent Game pointerconnection stateGame
white client or computerblack client or computerFENin-progress flagfinished stateServer
listenerclient collectionwaiting-player collectionengine connectionengine mutexshared-state mutexesEngine
child PIDwrite streamread streamtransaction lockThe exact C structures can vary.
What matters is that ownership is visible from the model.
The Architecture I Would Prefer Today
I would still keep the overall thread-per-client model because it matches the scale of the problem well.
But I would make a few design rules explicit.
Keep socket parsing outside game logic
A function that updates a chess game should not also be responsible for splitting TCP lines.
Keep UCI parsing behind an engine API
The rest of the server should ask questions such as:
is this move valid?what is the resulting FEN?what are the legal moves?what is the best move?is the side in check?rather than manually scanning Stockfish output everywhere.
Keep matchmaking operations atomic
Search + remove + create-game should be one protected state transition.
Keep lock scope understandable
The engine lock may need to cover a complete UCI transaction.
A game-state lock should not be held while waiting unnecessarily on slow I/O unless the consistency requirement demands it.
Treat messages to human opponents as cross-thread communication
One client handler may need to send a notification to another client’s socket.
That should be an intentional capability of the data model, not an accidental global variable.
What I Would Improve in a Second Version
1. Add a dedicated engine worker thread
Instead of allowing client threads to directly acquire the Stockfish lock, I would consider giving the engine its own request queue.
The architecture becomes:
client threads ↓engine-request queue ↓single engine worker ↓StockfishThis serializes access naturally and makes the engine an active service rather than a locked resource.
2. Use typed internal requests
The network protocol is text.
The internal server representation does not need to be.
I would parse incoming messages immediately into structured requests such as:
START_GAMEBOARD_REQUESTHINT_BESTHINT_ALLMOVERESIGNThat reduces repeated string handling inside domain logic.
3. Centralize game transitions
I would place all game lifecycle changes behind a small API:
game_start()game_apply_move()game_resign()game_disconnect()game_finish()That makes it harder for two different request handlers to update state inconsistently.
4. Add stronger runtime instrumentation
I would measure:
connected clientswaiting playersactive gamesengine request latencycommands per clientlock wait timeConcurrency problems are much easier to diagnose when the program can explain what it is doing.
5. Test the server independently from the official client
Because the protocol is text based, I would use scripted raw TCP tests for:
malformed commandsrapid reconnectstwo clients moving at nearly the same timedisconnect during a gameengine failurehuman colour matchingThat kind of testing is more effective than relying only on manual gameplay.
Final Thoughts
This project looked like a chess application, but most of the difficult reasoning had nothing to do with chess strategy.
The real questions were:
- Who owns the board state?
- How do several client threads share one engine?
- How should two human clients be paired?
- What must be protected by synchronization?
- Which failures affect one session and which affect the whole service?
- How do I preserve state when the executing thread changes?
- Where should protocol parsing stop and domain logic begin?
Those questions appear in many real systems.
A database server, build service, multiplayer game, hardware-control daemon, or model-serving backend can all have the same underlying shape:
many clients ↓concurrent request handling ↓shared domain state ↓serialized access to a limited resourceThe most useful lesson I kept from the project is:
Concurrency becomes manageable when state ownership is explicit and communication boundaries are designed before the threads are added.
The threads themselves are not the architecture.
The architecture is the set of rules that determines how those threads are allowed to interact.
Support & Share
If this article helped you, please share or support!
My Blog