02 — HTTP and Web Services

Not every client speaks gRPC. Browsers do not. Many legacy internal services do not. A significant chunk of your job as an applied C++/ML engineer is fronting your fast internal binary with a boring old REST or WebSocket surface so the rest of the org can talk to it. This file surveys the C++ HTTP landscape as it actually looks in 2026 and tells you what to pick.

The 2026 landscape, honestly

The C++ web framework market is small and stable. Three names show up in real production stacks. Pick based on the axis you care about (raw throughput vs learning curve vs binary size), not vibes.

Framework

Sweet spot

Async model

Learning curve

2026 vitality

drogon

High-throughput HTTP/1.1 + HTTP/2 REST, WebSocket, ORM baked in

Coroutines + callbacks on top of its own Trantor loop

Medium-high

Very active, benchmarks near the top of HttpArena

crow

Middle ground, header-only-ish, Flask-shaped API

Boost.Asio

Easy

Active but slower cadence

cpp-httplib

Single-header, dev-speed, internal tools

Blocking (thread-per-connection by default)

Trivial

Very active, cited everywhere for prototypes

There is a fourth category — Boost.Beast — which is what you use if you want to build your own framework. It is Asio + HTTP protocol primitives, not a framework. Serious houses (trading, browser vendors) use it. You do not need it in Phase 6.

Honest recommendation for your Phase 6 REST surface: drogon for P6.1’s REST facade, httplib for local dev tools and admin endpoints. Do not learn all three deeply; learn drogon and keep httplib in your back pocket.

drogon: the one to know

Install via brew install drogon on macOS or vcpkg on Linux. Minimal service:

#include <drogon/drogon.h>
using namespace drogon;

int main() {
  app().registerHandler(
      "/predict",
      [](const HttpRequestPtr& req,
         std::function<void (const HttpResponsePtr&)>&& callback) {
        auto resp = HttpResponse::newHttpJsonResponse(
            Json::Value(Json::objectValue));
        callback(resp);
      },
      {Post});
  app().addListener("0.0.0.0", 8080)
       .setThreadNum(std::thread::hardware_concurrency())
       .run();
}

Key drogon concepts you must know for P6.1:

  • Controllers. Class-based routes with reflection-style registration macros (METHOD_ADD). Cleaner than raw registerHandler past 5 routes.

  • Coroutines. drogon::Task<HttpResponsePtr> with co_await on database, HTTP client, or gRPC calls. Requires C++20 and a compiler with coroutines (Clang 14+, GCC 11+). Use this for the REST → gRPC bridge in P6.1.

  • Filters. Middleware. Auth, logging, CORS all go here.

  • ORM. drogon includes an ORM. Do not use it in Phase 6; you have no database. Ignore.

Drogon is measurably fast. Independent HttpArena benchmarks (2025) put it in the top-10 across 30 frameworks and #1 on HTTP/2 baseline at 64 connections. The Sharkbench aggregate shows ~7.2K RPS on a modest workload where Spring Boot does ~1.1K. These numbers should not be your reason to pick it — the reason is that its API is coroutine-native and its author is responsive. But it’s useful to have real numbers on hand.

httplib: the emergency parachute

cpp-httplib is one header. #include <httplib.h> and you have a server. Its speed is not competitive with drogon under load, but its ergonomics for internal tools are unbeatable.

#include <httplib.h>
httplib::Server svr;
svr.Get("/health", [](const httplib::Request&, httplib::Response& res) {
  res.set_content("{\"ok\":true}", "application/json");
});
svr.listen("0.0.0.0", 8081);

Use httplib for:

  • Admin endpoints on P6.1 (/health, /version, /reload-config).

  • Local test harnesses.

  • Anything where “I need an HTTP endpoint in 5 minutes” beats “I need 100K RPS.”

Do not use httplib for the user-facing surface of a real service. It is blocking; heavy load pins threads.

WebSockets

drogon supports WebSocket handlers via WebSocketController. You will need this in P6.1 if you expose a streaming inference endpoint to browser clients (which you should, at least as an optional acceptance criterion).

class StreamPredict : public WebSocketController<StreamPredict> {
 public:
  void handleNewMessage(const WebSocketConnectionPtr& conn,
                        std::string&& msg,
                        const WebSocketMessageType& type) override;
  void handleNewConnection(const HttpRequestPtr&,
                           const WebSocketConnectionPtr&) override;
  void handleConnectionClosed(const WebSocketConnectionPtr&) override;
  WS_PATH_LIST_BEGIN
  WS_PATH_ADD("/stream");
  WS_PATH_LIST_END
};

Backpressure matters here. If the browser is slow to consume, your server-side send buffer grows. drogon exposes conn->send() return value — check it, and drop or slow producer accordingly.

JSON in 2026

The C++ JSON space has changed in the last two years. Here is the honest ranking:

Library

Speed

Ergonomics

When to use

nlohmann::json

Slow (baseline)

Best

Config files, admin endpoints, any code where the JSON is not the bottleneck

simdjson

Very fast (parse only)

Awkward (DOM-like read-only)

Parsing large incoming payloads you only read

glaze

Faster than simdjson on serialize round-trip

Excellent (reflection-based, C++20)

New code you own end-to-end; strongly typed schemas

RapidJSON

Fast

OK

Legacy code; do not start new projects here

Rule of thumb: nlohmann is still the default because the ecosystem knows it. Reach for simdjson when a profiler tells you JSON parsing is your hotspot. Reach for glaze when you own the schema and want serialize + parse both fast without writing DOM code. Do not sprinkle three JSON libraries across one binary — pick one for the hot path and stick.

Glaze note: it beats simdjson on some real workloads because it goes struct-to-JSON directly, no intermediate DOM. If you have not looked at glaze since 2023, look again — it is the fastest general-purpose C++ JSON library in independent benchmarks as of late 2025.

CORS, auth headers, streaming responses

Three cross-cutting concerns for your REST facade:

CORS. In drogon, use a filter that sets Access-Control-Allow-Origin, -Methods, -Headers and short-circuits OPTIONS preflights. Do not allow * in production; enumerate the origins you actually need.

Auth headers. For P6.1, a simple Authorization: Bearer <token> filter is enough. Compare the token against an env-injected secret. Do not roll JWT parsing yourself in this phase — wire in jwt-cpp if the requirement escalates.

Streaming responses. drogon HttpResponse::newStreamResponse lets you produce a chunked HTTP response as inference proceeds. Useful for slow models where the client wants tokens as they generate (LLM-shaped workloads). httplib supports this too via set_chunked_content_provider.

Framing your REST facade around gRPC

P6.1’s design in one picture:

[browser/curl]  ── HTTPS ──▶  [drogon REST /predict]  ── gRPC ──▶  [inference core]
                                    │
                                    ├── /health   (httplib admin)
                                    ├── /metrics  (prometheus-cpp)
                                    └── /openapi.json (static)

The REST layer’s only jobs are: parse JSON, translate to Protobuf, forward, translate the reply back to JSON, add trace headers, return. Keep it thin. The moment your REST layer starts containing model logic, you have made a mistake — push that logic behind the gRPC boundary.

What most people get wrong

  • Rolling their own HTTP server on top of raw Asio because “frameworks are bloat.” Then re-implementing CORS, chunked encoding, keep-alive, and HTTP/2 badly. Use drogon.

  • Using nlohmann on a hot path that parses 10MB payloads at 5K RPS. Profile before you swap; then swap to simdjson or glaze.

  • Forgetting to disable HTTP for TLS-mandatory endpoints. Always redirect :80 :443 or refuse :80 entirely on public deployments.

  • Blocking calls inside coroutine handlers. std::this_thread::sleep_for inside a drogon coroutine will stall the event loop. Use co_await drogon::sleepCoro.