u/boostlibs

▲ 170 r/cpp

Boost 1.92.0 released

Boost 1.92.0 is out. Highlights from this release:

GPU / CUDA

  • Charconv: to_chars and from_chars for integers are now usable inside CUDA kernels.
  • Decimal: decimal32_t, decimal64_t, and decimal128_t are now usable in CUDA kernels.
  • Math: fixed CUDA compilation where host functions were incorrectly marked as device.

Networking hardening

  • Beast: stricter HTTP parsing. It now rejects Content-Length combined with Transfer-Encoding regardless of field order, rejects chunked encoding in HTTP/1.0 requests, validates quoted strings in chunk extensions, and drops framing and connection fields carried in trailers. The dependency on Boost.Functional was also removed.
  • URL: third round security review fixes, including a heap buffer overflow in normalize_path for authority-less URLs and an uninitialized read in ipv6_address_rule.

Containers and data structures

  • Container: new hub container designed by Joaquín M. López Muñoz. Also adds unchecked_emplace_back and unchecked_push_back to vector, static_vector, and small_vector.
  • Lockfree: two new queues, mpsc_weak_queue (MPSC) and bounded_ticket_queue (ringbuffer based bounded MPMC). Both have explicit progress caveats: neither is strictly lock free under all configurations.
  • Unordered: C++20 ranges interop across all containers (insert_range, std::from_range construction, and associated CTAD).
  • Graph: Louvain community detection for modularity based clustering.
  • Hash2: built in support for std::optional, std::variant, and std::monostate.

C++20 modules

  • New module support in Conversion, DLL, LexicalCast, PFR, Stacktrace, and TypeIndex.

Build system

  • Windows .dll files now install into the binary directory by default (previously the library directory, except on Cygwin). A new --dlldir option overrides this.
  • The CMake config installed by b2 install now supports header only libraries as find_package components, so find_package(Boost REQUIRED COMPONENTS mp11) works and defines Boost::mp11.

Deprecations and breaking changes

  • Heap and Lockfree: this is the last release to support C++14. Future releases require C++17.
  • MSM (backmp11) has several breaking changes, notably that events in process_event are no longer enqueued automatically. Use enqueue_event in actions instead.

Full notes and downloads: https://www.boost.org/releases/1.92.0/

reddit.com
u/boostlibs — 8 days ago
▲ 78 r/cpp

Introducing the Boost Documentary! Teaser & CppCon Preview

"If I were to tell a story about Boost, I'd start with the people."

Today we're sharing the official teaser for the Boost documentary. A film about the people, the politics, and decades of work behind possibly the most important open source library most people have never heard of.

Teaser link – https://youtu.be/87jvuDbnwqQ

The documentary looks at:

  • Boost as a kind of "app store for C++, 30 years early"
  • What decades of open source dedication looks like up close
  • The honest, sometimes uncomfortable dynamics of how proposals and people move through the C++ committee

There will be a preview screening at CppCon 2026 for all attendees. So if you're going to be in Aurora, CO September 16, 2026, please join us!

u/boostlibs — 2 months ago
▲ 12 r/cpp

Why we put chat messages in Redis streams (and plan to move old ones to MySQL)

The BoostServerTech Chat project stores every message in Redis. An in-memory data store that Rubén Pérez (@anarthal) already knows will need to be replaced for older messages down the road.

He did it anyway. Here's why and what the code looks like.

Rubén is the author of Boost.MySQL and co-maintainer of Boost.Redis. He built this chat server as a case study in composing Boost libraries for a real application.

The fit

Chat messages have a specific access pattern: append only, read backward (newest first), scoped to a room. Redis streams match this almost exactly. Each room (chat group) is a stream. Writing a message is XADD. Reading history is XREVRANGE. Redis assigns each entry a unique, time ordered ID, so you get message ordering and cursor-based pagination for free. No schema migrations, indexing decisions, or ORM.

A SQL table could do this. But messages are generated at a fast pace and most SQL databases would struggle with this insertion heavy flow. It would require serious performance tuning for a workload that Redis handles natively.

Storing a message

When a user sends a message, the server appends it to the room's Redis stream. The "*" tells Redis to auto assign a stream ID:

// Compose the request. XADD appends to the room's stream
// and auto-assigns an ID.
redis::request req;
for (const auto& msg : messages)
    req.push("XADD", room_id, "*", "payload",
             serialize_redis_message(msg));
// Execute it. All XADDs go out in one round trip.
redis::generic_response res;
error_code ec;
co_await conn_.async_exec(req, res, asio::redirect_error(ec));

Three things worth noting:

  1. Multiple XADD commands get pushed into a single redis::request. Boost.Redis pipelines them over one connection, so even if a client sends several messages at once, it's one round trip.
  2. This is a C++20 coroutine. The co_await suspends until Redis responds, but the thread is free to handle other work while it waits.
  3. XADD accepts an arbitrary list of (key, value) string pairs. We are using a single key named “payload” that contains the message serialized as JSON. This allows arbitrary nesting.

Serialization without boilerplate

Each message is stored as a JSON payload inside the stream entry. The wire format is a simple struct:

struct redis_wire_message
{
    std::string_view content;
    std::int64_t timestamp;
    std::int64_t user_id;
};
BOOST_DESCRIBE_STRUCT(redis_wire_message, (), (content, timestamp, user_id))

That BOOST_DESCRIBE_STRUCT macro registers the struct's members for compile time reflection. Boost.JSON picks it up automatically: boost::json::value_from(msg) serializes it, boost::json::try_value_to<redis_wire_message>(jv) deserializes it. No hand-written to_json/from_json functions. Add a field to the struct and the serialization updates itself.

This is one of those spots where Boost libraries click together in a way that's hard to replicate with unrelated dependencies. Describe provides the reflection, JSON consumes it. Three lines replace what would otherwise be two hand maintained serialization functions.

The tradeoff

Redis keeps everything in memory. That's what makes it fast, and it's also the obvious problem. Right now, the server runs with Redis persistence enabled, so data survives restarts. But as message volume grows, keeping the full history in RAM stops making sense.

The plan is to eventually offload old messages to MySQL for archival. The message layer is already isolated behind its own service interface, so swapping in a tiered storage strategy (recent messages from Redis, older ones from MySQL) touches one component. Nothing else needs to know.

But "eventually" involves a lot. The migration boundary is full of questions. Do you move messages after a time window? After a count threshold? Do you do it inline during reads, or as a background job? What happens to cursor based pagination when the data lives in two places?

If you've built a system that migrated data from a fast ephemeral store to a slower durable one, what triggered the migration and what surprised you about it? Rubén is interested in hearing what actually worked.

reddit.com
u/boostlibs — 2 months ago
▲ 39 r/cpp

How a Chat Server Talks to Everything: Designing the Interface Layer

Before Rubén Pérez (@anarthal) started writing code for the BoostServerTech Chat project, he had to figure out how everything would talk to everything else. The browser to the server. The server to Redis, MySQL, and an in-memory broadcast system. And so on.

Rubén is the author of Boost.MySQL and co-maintainer of Boost.Redis. He built this chat server as a case study in leveraging Boost libraries.

The first real design work had nothing to do with Boost. It was drawing the boundaries between systems and deciding what the messages between them look like.

There’s no C++ in this post, just the interface design that came first.

App features

Before diving into what the API should contain, we should first ask: “what do we want to support?”. There is a myriad of features that can be interesting, so we need to focus.

Ruben chose to start simple:

  • Users can create their own accounts and login with a username and password.
  • Users participate in group chats, called rooms. Rooms are currently static.

Two protocols, one server

Account creation and login are one-shot operations. The client sends a request and waits for a response. HTTP is fine for this.

Chat messages are different. When someone types something in a room, every connected client needs to see it right away. WebSockets give you a persistent connection where the server pushes data whenever it has something to say. So that’s what Rubén used.

The rule is simple: one-shot operations go over HTTP, real-time interaction goes over WebSocket.

The HTTP surface ended up tiny. Just two endpoints:

  • POST /api/create-account for self-registration
  • POST /api/login for authentication

Everything else goes through WebSocket.

The WebSocket protocol

A WebSocket is just a bidirectional pipe. You still need a message format. Rubén went with a simple envelope: every message is a JSON object with a type field and a payload field. Type tells you what it is, payload carries the data. One dispatch point on each side and easy to extend later.

Connection: the hello event

When a client opens a WebSocket connection, the server sends back a hello event. It contains everything the UI needs to render: the authenticated user, the room list, and recent message history for each room.

So there are no follow-up REST calls. The client connects once and has a fully populated screen. The tradeoff is a fat initial payload, but with a fixed set of rooms and a capped history window it stays manageable.

This is what a hello event looks like:

{
  "type": "hello",
  "payload": {
    "me": { "id": 1, "username": "alice" },
    "rooms": [
      {
        "id": "beast",
        "name": "Boost.Beast",
        "messages": [
          {
            "id": "1697312400000-0",
            "content": "Has anyone tried the new...",
            "user": { "id": 2, "username": "bob" },
            "timestamp": 1697312400000
          }
        ],
        "hasMoreMessages": true
      }
    ]
  }
}

Broadcasting messages in real time

clientMessages: sent by the client when the user hit send. Carries a room ID and an array of message objects (each just a content string). The array is there for extensibility, to allow batching.Currently, it’s always a single message.

serverMessages: the broadcast. When anyone sends a message, the server persists it, then pushes serverMessages to every connected client in that room, including the sender. Each message comes back with a server assigned ID, a timestamp, the content, and the sender's user info. The original sender uses this to confirm delivery.

WebSocket: clientMessages (client to server)

{
  "type": "clientMessages",
  "payload": {
    "roomId": "beast",
    "messages": [
      { "content": "This is my message" }
    ]
  }
}

WebSocket: serverMessages (server to client)

{
  "type": "serverMessages",
  "payload": {
    "roomId": "beast",
    "messages": [
      {
        "id": "1697312500000-0",
        "content": "This is my message",
        "user": { "id": 1, "username": "alice" },
        "timestamp": 1697312500000
      }
    ]
  }
}

Room History

The hello event contains only the most recent messages for each room, for efficiency reasons. Clients may request older messages with these messages:

requestRoomHistory: the user scrolled up past the messages loaded in hello. The client sends the room ID and the ID of the oldest message it has. The server responds with the next page of older messages. Cursor-based pagination basically.

roomHistory: the answer to requestRoomHistory. A batch of older messages plus a hasMoreMessages boolean so the client knows whether to keep paginating.

WebSocket: requestRoomHistory (client to server)

{
  "type": "requestRoomHistory",
  "payload": {
    "roomId": "beast",
    "firstMessageId": "1697312400000-0"
  }
}

WebSocket: roomHistory (server to client)

{
  "type": "roomHistory",
  "payload": {
    "roomId": "beast",
    "messages": [ ... ],
    "hasMoreMessages": false
  }
}

The HTTP API

The HTTP API handles authentication. Server-side, clients are authenticated with a session ID generated when the client authenticates using the /api/login endpoint and stored server-side. Client side, this session ID is stored in a cookie with the appropriate security attributes and sent to the server on subsequent requests.

Upon success, both /api/create-account and /api/login return a successful HTTP status and an empty response.  On error, they return a matching status and a JSON response with details to feed back to the end user.

HTTP: Create Account Request

{
  "username": "alice",
  "email": "alice@example.com",
  "password": "hunter2"
}

HTTP: Login Request

{
  "email": "alice@example.com",
  "password": "hunter2"
}

HTTP: Error Response

{
  "id": "EMAIL_EXISTS",
  "message": "An account with this email already exists"
}

Behind the server: three backend systems

The frontend contract is settled. Now how does the server actually fulfill it? Three systems, each owning one kind of data.

MySQL owns users. Account creation, credential lookups, resolving user IDs to usernames. If it’s about identity, it lives in MySQL. Messages don’t, at least not yet. Recall that MySQL is slower than Redis, but it provides the necessary ACID guarantees that identity management requires.

Redis owns messages. Each chat room is a Redis stream, an append only log. When the server stores a message, Redis assigns a stream ID. That becomes the message ID the client sees (those 1697312400000-0 strings in the JSON above). Redis also handles session storage: session ID mapped to user ID, with a 7-day TTL. When the key expires, the session is gone. So no cleanup job is needed.

An in-memory pub/sub system owns broadcast. After a message is persisted to Redis, the server publishes it through a process-local data structure. Every WebSocket client subscribed to that room gets the event immediately. This isn’t Redis pub/sub. It’s entirely in-process. That’s a direct consequence of the single-threaded, single-connection Asio architecture: one process, one thread, so an in-memory structure is both fast and safe without locking. It also means the server only works as a single instance. Rubén accepted that constraint deliberately. Replacing it with something distributed is on the roadmap.

Here’s the message flow when someone hits send:

  1. Client sends clientMessages over WebSocket
  2. Server stores the messages in the room's Redis stream
  3. Redis returns assigned IDs, server attaches timestamps
  4. Server looks up the sender's username. This is already in memory at this point, so no database lookup is required.
  5. Server publishes serverMessages through the in-memory pub/sub
  6. Every connected client in that room gets the broadcast

And the login flow:

  1. Client sends POST /api/login
  2. Server finds the user by email in MySQL
  3. Server checks the password hash (scrypt)
  4. Server generates a 16 byte session ID, stores it in Redis with 7-day TTL
  5. Server sends back a Set-Cookie (HttpOnly, SameSite=Strict)
  6. The WebSocket connection later includes that cookie in the HTTP upgrade request

Why split things this way

You could put everything in one database. But the access patterns in this case are clearly different: user data is relational and looked up by email or ID, messages are append only and read by range, and broadcast is ephemeral. Matching each backend to its access pattern keeps things clean, and it means each layer can change independently. The plan to eventually offload old messages from Redis to MySQL for archival only touches the message layer. Nothing else moves.

An open question

Right now the room list is hardcoded. Four rooms, defined at compile time: "Boost.Beast", "Boost.Async", "Database connectors", "Web assembly". Rubén did this to keep early development focused on the messaging pipeline. But it’s the most obvious thing to change. If you were adding dynamic room creation to a system like this, where would rooms live? A MySQL table? Redis, next to the streams? Something else? If you have built this, what worked?

Full source: github.com/anarthal/servertech-chat.

This is the second post in a series exploring the engineering decisions behind this project. The first, on the single-threaded Asio architecture, is here.

github.com
u/boostlibs — 3 months ago
▲ 60 r/cpp+1 crossposts

What Happens When You Build a Chat Server on One Thread?

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?

anarthal.github.io
u/boostlibs — 3 months ago
▲ 87 r/cpp

Boost 1.91.0 is now available in both Conan and vcpkg

For those of you waiting to upgrade through your package manager, Boost 1.91.0 has landed in both Conan and vcpkg.

What's in 1.91:

  • Boost.Decimal — new library implementing IEEE 754 decimal floating point arithmetic (from Matt Borland and Christopher Kormanyos)
  • Asio binary versioning — optional inline namespace lets multiple Asio versions coexist in the same process without symbol conflicts
  • 58 fewer internal dependencies across 55 libraries
  • StaticAssert merged into Config — no code changes needed, just update your dependency declarations when ready
  • CMake import std detection fix

Install:

conan install --requires=boost/1.91.0

vcpkg install boost

Links:

boost.org
u/boostlibs — 3 months ago