Building a Concurrent Echo Server with Crystal Fibers
Learn how Crystal’s lightweight fibers let you write concurrent network code that looks synchronous while staying on a single OS thread.
16 Aug 2026, 20:05 UTC

The Problem: Handling Many Clients Without OS Thread Overhead
When a server must serve many simultaneous connections, the naïve approach spawns a new OS thread (or process) for each client. Threads consume memory and involve costly context switches, limiting scalability.
How Crystal Fibers Provide a Lightweight Alternative
Crystal’s spawn creates a fiber that runs on the same OS thread as the caller. The fiber yields control when it performs a blocking operation that the runtime has hooked, such as reading from a socket. Because fibers are cooperative, switching between them costs only a few hundred nanoseconds and no locks are needed for shared state as long as only one fiber executes at a time.
Worked Example: A Simple Echo Server
require "socket"
def handle_client(client : TCPSocket)
while line = client.gets
client.puts line
end
ensure
client.close
end
server = TCPServer.new(8080)
puts "Echo server listening on port 8080"
while fiber_client = server.accept?
spawn { handle_client(fiber_client) }
end
Save this as fiber_example.cr, compile with crystal build fiber_example.cr, then run the binary. Open another terminal and connect with netcat localhost 8080; each connection is handled in its own fiber while the process remains single‑threaded.
To verify the thread count, find the PID of the running binary (e.g., pgrep -f fiber_example) and run ps -L -p <pid>. You should see a single line in the NLWP column, confirming only one OS thread is active despite many concurrent fibers.
Trade‑off: CPU‑Bound Work Blocks the Scheduler
If a fiber performs a long‑running computation without yielding—such as a tight loop calculating Fibonacci numbers—it occupies the sole OS thread, preventing other fibers from making progress. The solution is to offload heavy work to a separate thread via Thread or BlockingIO, or to insert explicit Fiber.yield calls at appropriate intervals.
Thus, while fibers excel at I/O‑bound concurrency, developers must ensure that CPU‑intensive sections either yield or run elsewhere to avoid stalling the entire scheduler.
Actionable Closing
Start by replacing thread‑per‑connection designs with spawn { ... } for I/O‑bound services. Monitor thread usage with ps -L to confirm the single‑threaded property, and profile any CPU‑heavy paths to add yields or move them to dedicated threads. This approach gives you the ergonomics of synchronous code with the scalability of lightweight concurrency.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.