The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.

drogonR — Three Ways to Serve HTTP from R

drogonR wraps the Drogon C++ HTTP framework and exposes it to R. The same server can be driven through three different APIs depending on how much R you want in the request hot path:

  1. dr_*_cpp() — C++ shared path. Handlers are pure C functions in another R package, resolved via R_GetCCallable() and called on Drogon’s worker threads. R is not in the hot path. Intended for inference packages (ggmlR, llamaR, sd2R) that already do their work in C++.
  2. dr_app() + dr_get() / dr_post() / … — drogonR native. Handlers are R functions; the bridge marshals each request onto the main R thread, runs the closure, and ships the response back. Full control over routes, middleware, response shape. Recommended for new APIs written in R.
  3. drogonR::pr_run() — plumber drop-in. Existing plumber::pr_run(pr) becomes drogonR::pr_run(pr) with no other changes. The shim translates the plumber router into drogonR routes and serves them via dr_serve(). Recommended when you have a plumber codebase and want a faster runtime without rewriting it.

The variants share the same Drogon server underneath; only the registration path differs.


Choosing a variant

cpp-shared (dr_*_cpp) native (dr_get) plumber shim (pr_run) plumber (baseline)
Handler language C / C++ R R (plumber convention) R
Calls into R per request 0 1 1 1
Code change vs plumber rewrite handlers in C rewrite using dr_app() one line (pr_run)
Best for inference / hot loops new R-side APIs existing plumber apps dev / one-offs

The cpp-shared variant is the only one that bypasses R entirely on the request path. The native and shim variants both run an R closure per request — the difference between them is the calling convention (drogonR-native gets a req object; the shim emulates plumber’s positional / named-arg matching).


Performance snapshot

wrk -t4 -c50 -d30s against four servers running the same two routes on localhost. Workload is intentionally trivial — /ping returns a fixed {"ok":true}, /ping-text returns "ok" — so the numbers measure the framework overhead, not the handler cost. See tools/bench/run.sh for the harness.

Variant /ping (JSON) /ping-text (plain)
drogonR cpp-shared 239 428 rps, 200 µs 234 753 rps, 202 µs
drogonR native 116 159 rps, 822 µs 218 163 rps, 252 µs
drogonR plumber-shim 94 400 rps, 591 µs 99 276 rps, 583 µs
plumber (baseline) 1 078 rps, 44.5 ms 1 069 rps, 44.9 ms

(rps = requests per second, single-host loopback; latency is the wrk average.) Two things to read out of this:

The shim is slower than native for the same workload — it pays the cost of plumber’s argument-matching convention (path / query / body lookup, default-serialiser dispatch) on every call. It’s still well above plumber itself because the I/O loop is C++.


A minimal example of each

Variant 1 — cpp-shared (in your inference package):

// In yourPackage/src/handlers.c
#include <drogonR.h>
#include <R_ext/Rdynload.h>

static int h_predict(const char *body, size_t body_len,
                     const char *query,
                     const char *const *path, size_t path_n,
                     const char *const *hdrs, size_t hdrs_n,
                     char **out_body, size_t *out_len,
                     int  *out_status, char **out_content_type) {
    /* run inference, fill out_body via malloc(), set status, ctype */
    return 0;
}

void R_init_yourPackage(DllInfo *dll) {
    R_RegisterCCallable("yourPackage", "predict", (DL_FUNC) h_predict);
}
# In your serving script
library(drogonR)
app <- dr_app() |>
  dr_post_cpp("/predict", "yourPackage", "predict")
dr_serve(app, port = 8080L)

Variant 2 — drogonR native:

library(drogonR)
app <- dr_app() |>
  dr_get("/users/:id", function(req) {
    dr_json(list(id = req$params[["id"]], ok = TRUE))
  })
dr_serve(app, port = 8080L)

Variant 3 — plumber shim:

# Existing plumber.R, unchanged:
library(plumber)
pr <- pr() |>
  pr_get("/users/<id>", function(id) list(id = id, ok = TRUE))

# Only this line changes — drogonR::, not plumber::
drogonR::pr_run(pr, port = 8080L)

Where to go next

These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.