MyNixOS website logo
Description

High-Performance HTTP Server for R via 'Drogon'.

Provides an 'R' interface to the 'Drogon' high-performance 'C++' 'HTTP' server framework (<https://github.com/drogonframework/drogon>). Offers a 'plumber'-style application programming interface for building 'REST' services from 'R' with substantially higher throughput.

drogonR

R-hub

High-performance HTTP server for R, powered by the Drogon C++ framework.

drogonR provides a plumber-style API for building REST services and APIs from R, with substantially higher throughput. The Drogon, Trantor and JsonCpp sources are bundled and built statically — no external installation of Drogon is required.

Status: 0.1.6, in development. Linux only. Windows source portability is in place; full Windows build is pending.

Architecture

[Drogon I/O threads]  →  [Lock-free queue]  →  [R main thread]
        ↑                                              ↓
  Accept HTTP                                  Execute R handler
  Parse request                                Build R response
  TLS / HTTP/2                                       ↓
        ←──────────── [Response callback] ←───────────
  • Drogon runs in its own C++ thread; R handlers are dispatched on the main R thread via a thread-safe queue.
  • C++-only routes can bypass the queue entirely.
  • Multi-process scaling is provided via forked workers (each worker has its own R session listening on the same port via SO_REUSEPORT).

Benchmarks

Same workload (GET /ping returning {"ok":true}, and GET /ping-text returning "ok"), four servers running side by side, one at a time, measured with wrk -t4 -c50 -d30s on AMD Ryzen 5 5600 (6 cores). drogonR runs with threads=4, single worker; plumber is single-threaded by design. The four columns are the three drogonR serving paths plus the plumber baseline:

  • cpp-shareddr_get_cpp(), handler is a C function in another R package, R is not in the request hot path.
  • nativedr_app() + dr_get(), handler is an R closure.
  • plumber-shimdrogonR::pr_run(plumber_obj), plumber router served via drogonR's dispatcher.
  • plumber — vanilla plumber::pr_run(), baseline.
drogonR cpp-shareddrogonR nativedrogonR plumber-shimplumber
/ping requests/sec239 428116 15994 4001 078
/ping avg latency200 µs822 µs591 µs44.5 ms
/ping-text requests/sec234 753218 16399 2761 069
/ping-text avg latency202 µs252 µs583 µs44.9 ms

Two things to read out of this:

  • The cpp-shared path leaves R entirely — its throughput is bounded by Drogon and the kernel, ~240k rps for a trivial handler.
  • Even when an R closure runs per request (native, shim), drogonR is ~90–220× plumber, because the I/O loop is C++ and requests are marshaled onto the main R thread once per dispatch tick instead of per request.

The bench scripts live at tools/bench/run.sh (all four servers) and tools/bench/profile.sh (single-route perf record -g flame). Reproduce with bash tools/bench/run.sh and ROUTE=/ping bash tools/bench/profile.sh. For an in-depth look at the three drogonR variants see vignette("drogonR", package = "drogonR").

Installation

From source (development)

# Once published:
# install.packages("drogonR")

# From a local checkout:
install.packages("/path/to/drogonR", repos = NULL, type = "source")

Build requirements

  • C++17 compiler (GCC ≥ 7, Clang ≥ 5)
  • GNU make
  • (optional) OpenSSL development headers for HTTPS support

The configure script auto-detects OpenSSL via pkg-config and falls back to a plain-HTTP build if it is not found. To force the choice:

R CMD INSTALL --configure-args="--with-openssl"    drogonR
R CMD INSTALL --configure-args="--without-openssl" drogonR

Quick start

library(drogonR)

app <- dr_app() |>
  dr_get("/health", function(req) {
    dr_json(list(status = "ok"))
  }) |>
  dr_get("/users/:id", function(req) {
    # Path parameters: req$params is a named character vector.
    # `:id`, `<id>` and `{id}` are accepted interchangeably.
    dr_json(list(user_id = req$params[["id"]]))
  }) |>
  dr_post("/predict", function(req) {
    body <- dr_body(req, as = "json")
    dr_json(list(prediction = model_predict(body$data)))
  }) |>
  dr_get("/login", function(req) dr_redirect("/auth/sso")) |>
  dr_get("/report.csv", function(req) {
    dr_file("/srv/reports/latest.csv", download_as = "Q3-report.csv")
  })

# Single-process serve.
dr_serve(app, port = 8080L, threads = 4L)

# When done:
dr_stop()

Response helpers

  • dr_text(body)text/plain; charset=utf-8
  • dr_html(body)text/html; charset=utf-8
  • dr_json(x)application/json, with a fast C++ path for the common shapes
  • dr_redirect(location, status = 302L) — sets Location: and an empty body
  • dr_file(path, download_as = NULL) — reads a file, auto-detects the MIME from a built-in table, optionally adds Content-Disposition: attachment

Multi-process workers

For inference-bound APIs, fork N R worker processes that share the listening port via SO_REUSEPORT. Each worker has its own R session, so per-worker state (models, caches) can be loaded once in on_worker_start:

dr_serve(app, port = 8080L, workers = 8L,
         on_worker_start = function() {
           model <<- readRDS("model.rds")
         })

dr_status()   # data frame of worker pids and liveness

dr_stop() SIGTERMs every worker (with SIGKILL fallback after 2s) and reaps them.

Backpressure

Under overload, the request queue between Drogon and R is bounded by max_queue (default 1024). Once full, incoming requests are rejected with 503 Service Unavailable directly from a Drogon I/O thread — no R-side cost — instead of growing memory unboundedly:

dr_serve(app, port = 8080L, max_queue = 256L)

Streaming responses

For Server-Sent-Events feeds, LLM token streams, or any endpoint where the client cares about first-byte time more than last-byte time, return a dr_stream() (or the SSE convenience wrapper dr_stream_sse()) instead of a normal response. The dispatcher opens a chunked response and pumps the generator on the main R thread one chunk at a time. On client disconnect the generator is called once with cancelled = TRUE so it can release per-stream state.

app <- dr_app() |>
  dr_get("/sse", function(req) {
    dr_stream_sse(
      state = list(i = 0L, n = 5L),
      generator = function(state, cancelled) {
        if (cancelled || state$i >= state$n) {
          return(list(data = "", state = state, done = TRUE))
        }
        state$i <- state$i + 1L
        list(data  = sprintf("tick %d", state$i),
             state = state, done = FALSE)
      })
  })

See vignette("streaming", package = "drogonR") for the full API, threading caveats, and cancellation contract.

Rate limiting

Cap how many requests are allowed in a rolling window. The check runs on the I/O thread before R is invoked; over-budget requests get HTTP 429 with a Retry-After header.

app <- dr_app() |>
  dr_get("/health",    function(req) "ok") |>
  dr_get("/api/users", function(req) dr_json(list(...))) |>
  # 100 req / 60 s, per-route, applied to anything under /api/
  dr_rate_limit(capacity = 100L, window = 60, routes = "/api/")

Algorithms: "sliding_window" (default), "fixed_window", "token_bucket". Scope: "per_route" (default — each matched route gets its own bucket) or "global" (one bucket shared across the match set). Per-IP throttling is intentionally out of scope — do that in a reverse proxy. See vignette("rate-limiting", package = "drogonR").

License

drogonR itself is released under the MIT license.

The package bundles the following third-party libraries, all under the MIT license, with their original copyright notices preserved:

  • Drogon © an-tao and contributors
  • Trantor © an-tao and contributors
  • JsonCpp © Baptiste Lepilleur and contributors

See LICENSE.note for details.

Metadata

Version

0.1.6

License

Unknown

Platforms (80)

    Darwin
    FreeBSD
    Genode
    GHCJS
    Linux
    MMIXware
    NetBSD
    none
    OpenBSD
    Redox
    Solaris
    uefi
    WASI
    Windows
Show all
  • aarch64-darwin
  • aarch64-freebsd
  • aarch64-genode
  • aarch64-linux
  • aarch64-netbsd
  • aarch64-none
  • aarch64-uefi
  • aarch64-windows
  • aarch64_be-none
  • arc-linux
  • arm-none
  • armv5tel-linux
  • armv6l-linux
  • armv6l-netbsd
  • armv6l-none
  • armv7a-linux
  • armv7a-netbsd
  • armv7l-linux
  • armv7l-netbsd
  • avr-none
  • i686-cygwin
  • i686-freebsd
  • i686-genode
  • i686-linux
  • i686-netbsd
  • i686-none
  • i686-openbsd
  • i686-windows
  • javascript-ghcjs
  • loongarch64-linux
  • m68k-linux
  • m68k-netbsd
  • m68k-none
  • microblaze-linux
  • microblaze-none
  • microblazeel-linux
  • microblazeel-none
  • mips-linux
  • mips-none
  • mips64-linux
  • mips64-none
  • mips64el-linux
  • mipsel-linux
  • mipsel-netbsd
  • mmix-mmixware
  • msp430-none
  • or1k-none
  • powerpc-linux
  • powerpc-netbsd
  • powerpc-none
  • powerpc64-linux
  • powerpc64le-linux
  • powerpcle-none
  • riscv32-linux
  • riscv32-netbsd
  • riscv32-none
  • riscv64-linux
  • riscv64-netbsd
  • riscv64-none
  • rx-none
  • s390-linux
  • s390-none
  • s390x-linux
  • s390x-none
  • sh4-linux
  • vc4-none
  • wasm32-wasi
  • wasm64-wasi
  • x86_64-cygwin
  • x86_64-darwin
  • x86_64-freebsd
  • x86_64-genode
  • x86_64-linux
  • x86_64-netbsd
  • x86_64-none
  • x86_64-openbsd
  • x86_64-redox
  • x86_64-solaris
  • x86_64-uefi
  • x86_64-windows