NeFut Logo NeFut
Admin Login

[C++ Magic] Chat Server on One Thread: Deep Dive into Performance and Limitations

Published at: 2026-05-30 07:51 Last updated: 2026-06-06 13:04
#C++ #Asynchronous #Boost

Rubén Pérez, the author of Boost.MySQL and co-maintainer of Boost.Redis, built a group chat server to demonstrate how Boost libraries work together in a real application. The project, called BoostServerTech Chat, runs a single C++ process that handles HTTP, WebSocket, Redis, and MySQL connections, all on one thread. This post covers the viability of that design, its practical implications, and its potential pitfalls.

The Stack

The server sits behind a React/Next.js frontend and communicates with two backing stores: Redis for chat messages and sessions (stored as streams), and MySQL for user accounts. The C++ process does everything else: serves static frontend files, exposes a REST API for login and account creation, and upgrades HTTP connections to WebSocket for real-time messaging. HTTP handles requests without tight latency requirements, like account creation and authentication. Messages go over WebSocket to keep latency low. When a user types a message, the frontend sends it to the server over WebSocket, which persists it to a Redis stream and broadcasts it to other connected clients.

Coroutines in Action

The server is fully asynchronous, using C++20 coroutines through Boost.Asio. Using coroutines allows you to write async code that reads like synchronous code, avoiding callback hell. Here’s a snippet from the HTTP session handler:

// Handle a regular HTTP request by querying
// the backend databases as required
http::message_generator msg = co_await handle_http_request( parser.release(), *state );
// Determine if we should close the connection
bool keep_alive = msg.keep_alive();
// Send the response
co_await beast::async_write( stream, std::move(msg), asio::redirect_error(ec) );

The key point is that when execution reaches co_await handle_http_request(...), the server sends a query to Redis or MySQL. The coroutine suspends until the database responds, allowing other work to run on the same thread. When the response arrives, the coroutine resumes from where it left off.

One Thread, No Locks

Here’s the event loop setup in main.cpp:

// The server is single-threaded, so we set the
// concurrency hint to 1
asio::io_context ctx(1);

One io_context, one thread calling ctx.run(). Every connection, every database call, every WebSocket frame goes through the same event loop. The payoff: shared mutable state needs zero synchronization. The server maintains an in-memory structure tracking which clients subscribe to which chat rooms. In a multi-threaded server, every access to that structure needs a strand, and getting multi-threaded Asio right is not trivial. Here, it is just a container. No locks, no races, no ordering bugs that surface under load at 2 AM. This works because all I/O is asynchronous, meaning a MySQL query does not block the thread.

Service Composition

All services live in a shared_state object passed to every session:

class shared_state {
    struct {
        std::string doc_root_;
        std::unique_ptr<redis_client> redis_;
        std::unique_ptr<mysql_client> mysql_;
        std::unique_ptr<cookie_auth_service> cookie_auth_;
        std::unique_ptr<pubsub_service> pubsub_;
    } impl_;
};

Each service is an interface with an async implementation behind it, keeping compilation fast. The Redis client holds a single persistent connection, as recommended by Boost.Redis docs. The MySQL client uses a connection pool. The pub/sub service is an in-memory container built on Boost.MultiIndex. They all share the same io_context, cooperating on one thread with no explicit coordination.

Limitations

The obvious limitation is the use of one CPU core. For a chat server, that is fine, as the thread spends most of its time waiting on network I/O. However, CPU-intensive work per request (like image processing, compression, heavy serialization) would block every other connection. A subtler limitation is horizontal scaling. The pub/sub state lives in memory, so you cannot run two server instances behind a load balancer and expect messages to reach all clients. Rubén tracks this as a known next step: replacing the in-memory pub/sub with Redis channels or XREAD groups to allow multiple instances to share broadcast state.

The Full Picture

The entire server is around 3,000 lines of C++. It composes key Boost libraries (Asio, Beast, Redis, MySQL, JSON, Describe, MultiIndex, URL, and Test) into an application you can fork, build with CMake, and deploy in Docker. No framework, no abstraction layer hiding the details. Every layer is in the source. The BoostServerTech Chat repo has the full code, build instructions, and architecture docs.

Blogger's Review: This project showcases the potential of single-threaded asynchronous server design, particularly for I/O-bound applications. However, the limitations when facing CPU-intensive tasks remind us to balance performance with complexity in design. Future expansion directions are worth monitoring.

Original Source: https://www.reddit.com/r/cpp/comments/1to5dg2/what_happens_when_you_build_a_chat_server_on_one/

[h] Back to Home