Rubén Pérez, author of Boost.MySQL and co-maintainer of Boost.Redis, built a group chat server to show how Boost libraries work together in a real application. A working server with authentication, persistent message history, real-time broadcasting, and a React frontend. Something you can fork and deploy.
The project is called BoostServerTech Chat. It runs a single C++ process that handles HTTP, WebSocket, Redis, and MySQL connections, all on one thread. This post covers why that design holds up, what it looks like in practice, and where it comes apart.
The Stack
The server sits behind a React/Next.js frontend and talks to two backing stores: Redis for chat messages and sessions (stored as streams), and MySQL for user accounts. The C++ process does everything else: serves the 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. The server persists it to a Redis stream and broadcasts it to other connected clients.
What Coroutines Look Like Here
The server is fully asynchronous, using C++20 coroutines through Boost.Asio. If you haven't used them: you write async code that reads like synchronous code. You get the performance of asynchrony without the callback tangle.
Here is 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) );Full source: server/src/http_session.cpp
Don't worry about every detail here. The key point: when execution reaches co_await handle_http_request(...), the server sends a query to Redis or MySQL. The coroutine suspends until the database responds. Meanwhile, other work runs on the same thread. When the response arrives, the coroutine picks up right where it left off.
Compare this to callback-based Asio code. The same logic used to require nested lambdas, explicit state machines, and careful lifetime management. Coroutines flatten all of that into something that reads like a straight line.
One Thread, No Locks
Here is 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);Full source: server/src/main.cpp
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 keeps 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. A MySQL query does not block the thread. It yields, other coroutines run, and when the response arrives, the original coroutine resumes.
How Services Compose
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_; };Full source: server/include/shared_state.hpp
Each service is an interface with an async implementation behind it, which keeps compilation fast. The Redis client holds a single persistent connection, as the Boost.Redis docs recommend. 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.
Where This Breaks Down
The obvious limitation: one CPU core. For a chat server, that is fine. The thread spends nearly all its time waiting on network I/O. But CPU-intensive work per request (image processing, compression, heavy serialization) would block every other connection.
The subtler limitation: 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 so multiple instances can share broadcast state.
Then there is the middle ground: would an io_context backed by a small thread pool with strands give meaningfully better throughput on a single machine? That is tracked as issue #25, with measurements still pending.
For anyone curious about where async C++ server design is heading more broadly, the Corosio project explores similar coroutine patterns in a different context.
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. Rubén will be in the comments.
A question worth discussing: for I/O-bound services like this, is there a real-world case where a multi-threaded io_context with strands earns its complexity? Or is single-threaded the right default until measurements say otherwise?
[link] [comments]













English (US) ·