2026-04-22·9 min read·sota.io team

Deploy R to Europe — EU Hosting for Plumber APIs and Statistical Backends in 2026

R is the language that Europe's statistical institutions run on. Eurostat, the European Commission's statistical office in Luxembourg, publishes R packages for working with its datasets. The European Central Bank uses R for monetary policy research. The European Medicines Agency requires R in clinical trial submissions. Across the EU's universities, central banks, and national statistics offices, R is the standard tool for the kind of serious quantitative work that governments and financial institutions depend on.

This reach did not happen by accident. A substantial part of what makes R work in production — its package ecosystem, its repository infrastructure, its statistical computing libraries — was built by European contributors.

Martin Maechler at ETH Zürich in Switzerland has been an R Core Team member since 1997 and maintains the foundational Matrix package, which underpins much of modern statistical computing in R. His sfsmisc package and contributions to numerical stability have shaped how R handles the edge cases that matter in real scientific computation. ETH Zürich, one of Europe's foremost technical universities, has been an R development hub for three decades.

Kurt Hornik at WU Wien — the Vienna University of Economics and Business — is another R Core Team member who co-founded CRAN, the Comprehensive R Archive Network, which today hosts over 21,000 packages and serves millions of downloads per day. Without CRAN, R's package ecosystem would not exist in its current form. Hornik has also maintained the NLP infrastructure and text mining packages that power much of R's natural language processing work.

Peter Dalgaard at Copenhagen Business School in Denmark contributed core statistical functions and has been part of the R Core Team since the early 2000s. Torsten Hothorn at the University of Zurich developed the coin package for conditional inference and contributes to survival analysis tooling used in clinical research across EU pharmaceutical companies.

Perhaps no single European has done more to extend R's reach into production systems than Dirk Eddelbuettel, a German developer who created Rcpp, the package that allows R to call C++ code with minimal overhead. Rcpp is one of the most downloaded R packages of all time — it has been installed billions of times and is a dependency for roughly a third of all CRAN packages. When R code needs to be fast, it usually goes through Rcpp. Eddelbuettel also created RQuantLib, the R bindings to QuantLib that are used in European financial institutions for derivatives pricing.

For teams at EU institutions or companies whose data lives in EU infrastructure, deploying R APIs to European servers is not just a compliance preference — it is a data sovereignty requirement. This guide shows how to build and deploy an R Plumber API to EU infrastructure with sota.io.

Why R for Production APIs

The case for deploying R as a backend API service is strongest when the data science team and the engineering team are the same people — or when the model logic is too complex to translate safely into another language.

Reproduce what you compute. If your statistical model was built in R, your API should run in R. Translating a complex mixed-effects model, a survival analysis, or a Bayesian hierarchical model from R into Python or Go introduces transcription errors that are hard to detect and expensive to debug. The model that serves predictions should be the model that was validated.

The full statistical ecosystem. R has packages for econometrics (AER, plm), time series (forecast, tseries, xts), survival analysis (survival, flexsurv), clinical trial methods (coin, randomForest), spatial statistics (sf, terra), and hundreds of specialised fields that have no equivalent in other languages. If your domain is statistics, R's package ecosystem is deeper than anything else.

Institutional compatibility. EU public health agencies, central banks, and national statistics offices publish data in formats designed for R — .rds files, R package interfaces to their APIs, R-based reporting templates. Working in the same ecosystem reduces integration friction.

Plumber turns R functions into HTTP endpoints. The Plumber package, maintained by the team at Posit, lets you annotate R functions with roxygen-style comments to declare them as HTTP handlers. An R function with a #* @get /predict comment becomes a GET endpoint. The framework handles routing, serialisation, authentication middleware, and OpenAPI documentation generation automatically.

Building a Plumber API

A minimal Plumber API exposes R functions via HTTP:

# plumber.R
library(plumber)
library(DBI)
library(RPostgres)

# Health check
#* @get /health
function() {
  list(status = "ok", timestamp = Sys.time())
}

# Predict endpoint — takes JSON body with feature vector
#* @post /predict
#* @param features:object Feature vector for the model
function(features) {
  model <- readRDS("/app/model.rds")
  df <- as.data.frame(features)
  prediction <- predict(model, newdata = df, type = "response")
  list(
    prediction = as.numeric(prediction),
    confidence = attr(prediction, "conf.int")
  )
}

# Statistical summary endpoint
#* @get /stats/<dataset>
#* @param dataset:str Dataset identifier
function(dataset) {
  con <- dbConnect(
    RPostgres::Postgres(),
    host     = Sys.getenv("PGHOST"),
    port     = as.integer(Sys.getenv("PGPORT", "5432")),
    dbname   = Sys.getenv("PGDATABASE"),
    user     = Sys.getenv("PGUSER"),
    password = Sys.getenv("PGPASSWORD")
  )
  on.exit(dbDisconnect(con))

  result <- dbGetQuery(
    con,
    "SELECT * FROM datasets WHERE name = $1 LIMIT 10000",
    params = list(dataset)
  )

  list(
    n          = nrow(result),
    mean       = colMeans(result[sapply(result, is.numeric)], na.rm = TRUE),
    sd         = sapply(result[sapply(result, is.numeric)], sd, na.rm = TRUE),
    quantiles  = lapply(
      result[sapply(result, is.numeric)],
      quantile, probs = c(0.25, 0.5, 0.75), na.rm = TRUE
    )
  )
}

The entrypoint starts the server:

# main.R
library(plumber)

pr <- plumb("plumber.R")

pr$run(
  host   = "0.0.0.0",
  port   = as.integer(Sys.getenv("PORT", "8080")),
  docs   = FALSE,   # disable Swagger UI in production
  debug  = FALSE
)

Connecting to PostgreSQL

R's database connectivity uses DBI (a standard database interface) with driver packages. For PostgreSQL:

# db.R — connection pooling with pool package
library(pool)
library(DBI)
library(RPostgres)

# Create a pool — reused across requests
db_pool <- dbPool(
  drv      = RPostgres::Postgres(),
  host     = Sys.getenv("PGHOST"),
  port     = as.integer(Sys.getenv("PGPORT", "5432")),
  dbname   = Sys.getenv("PGDATABASE"),
  user     = Sys.getenv("PGUSER"),
  password = Sys.getenv("PGPASSWORD"),
  minSize  = 2,
  maxSize  = 10
)

# Graceful shutdown
onStop(function() {
  poolClose(db_pool)
})

# Query helper
query_df <- function(sql, params = list()) {
  dbGetQuery(db_pool, sql, params = params)
}

Using the pool in Plumber handlers:

# plumber.R (with pool)
source("db.R")

#* @get /timeseries/<indicator>
#* @param indicator:str Eurostat indicator code
#* @param from:str Start date (YYYY-MM-DD)
#* @param to:str End date (YYYY-MM-DD)
function(indicator, from = "2020-01-01", to = Sys.Date()) {
  result <- query_df(
    "SELECT date, value, unit
     FROM eurostat_cache
     WHERE indicator = $1
       AND date BETWEEN $2::date AND $3::date
     ORDER BY date",
    params = list(indicator, from, to)
  )

  if (nrow(result) == 0) {
    stop(sprintf("No data found for indicator %s", indicator))
  }

  # Return as time series summary
  list(
    indicator = indicator,
    from      = min(result$date),
    to        = max(result$date),
    n         = nrow(result),
    series    = result
  )
}

Middleware and Authentication

Plumber supports middleware filters — R functions that run before every request:

# plumber.R

# Bearer token authentication filter
#* @filter auth
function(req, res) {
  # Allow health checks without auth
  if (req$PATH_INFO == "/health") {
    plumber::forward()
    return()
  }

  auth_header <- req$HTTP_AUTHORIZATION
  if (is.null(auth_header) || !startsWith(auth_header, "Bearer ")) {
    res$status <- 401
    return(list(error = "Unauthorised"))
  }

  token <- substring(auth_header, 8)
  expected <- Sys.getenv("API_TOKEN")

  if (!identical(token, expected)) {
    res$status <- 403
    return(list(error = "Forbidden"))
  }

  plumber::forward()
}

# Request logging filter
#* @filter logger
function(req, res) {
  message(sprintf(
    "[%s] %s %s",
    format(Sys.time(), "%Y-%m-%dT%H:%M:%SZ"),
    req$REQUEST_METHOD,
    req$PATH_INFO
  ))
  plumber::forward()
}

Error Handling

Plumber converts R errors to HTTP 500 responses by default. Override this with a custom error handler:

# main.R

pr <- plumb("plumber.R")

pr$setErrorHandler(function(req, res, err) {
  message("Error: ", conditionMessage(err))

  # Map R condition classes to HTTP status codes
  status <- 500L
  if (inherits(err, "not_found_error")) status <- 404L
  if (inherits(err, "validation_error")) status <- 400L

  res$status <- status
  list(
    error   = conditionMessage(err),
    path    = req$PATH_INFO,
    method  = req$REQUEST_METHOD
  )
})

pr$run(host = "0.0.0.0", port = as.integer(Sys.getenv("PORT", "8080")))

Define custom condition classes in your handlers:

not_found_error <- function(message) {
  structure(
    class = c("not_found_error", "error", "condition"),
    list(message = message)
  )
}

#* @get /model/<id>
function(id) {
  model_path <- sprintf("/app/models/%s.rds", id)
  if (!file.exists(model_path)) {
    stop(not_found_error(sprintf("Model '%s' not found", id)))
  }
  readRDS(model_path)
}

Dockerfile

FROM rocker/r-ver:4.3.3

# System dependencies for PostgreSQL and common R packages
RUN apt-get update && apt-get install -y \
    libpq-dev \
    libssl-dev \
    libcurl4-openssl-dev \
    libxml2-dev \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Install R packages — cached as a layer
COPY packages.R .
RUN Rscript packages.R

COPY . .

EXPOSE 8080

CMD ["Rscript", "main.R"]

The packages.R installs dependencies:

# packages.R
install.packages(c(
  "plumber",
  "DBI",
  "RPostgres",
  "pool",
  "jsonlite",
  "logger"
), repos = "https://cloud.r-project.org", quiet = TRUE)

Deploying to sota.io

sota.io detects R applications from the Dockerfile and deploys them to EU infrastructure.

1. Create a sota.yaml:

name: r-api
region: eu-central
build:
  dockerfile: Dockerfile
env:
  PORT: "8080"
  PGHOST: ${PGHOST}
  PGPORT: ${PGPORT}
  PGDATABASE: ${PGDATABASE}
  PGUSER: ${PGUSER}
  PGPASSWORD: ${PGPASSWORD}
  API_TOKEN: ${API_TOKEN}
health_check:
  path: /health
  interval: 30s

2. Deploy:

sota deploy

That's it. sota.io provisions EU servers, builds the Docker image, runs database migrations if configured, and serves your Plumber API from Frankfurt, Amsterdam, or Stockholm.

Why EU Hosting Matters for R Data APIs

For teams working with EU institutional data, the data residency argument is not abstract:

Eurostat APIs. Eurostat's REST API returns data about EU economies, populations, and trade. When you cache this data in PostgreSQL and serve it via an R API, the entire data pipeline — source, storage, API — should remain within EU jurisdiction. GDPR Article 44 restricts transfers of personal data to third countries. Even for aggregate statistical data, keeping the processing infrastructure in the EU simplifies compliance analysis.

ECB and national central bank data. The European Central Bank's Statistical Data Warehouse provides macroeconomic time series used by R packages like ecb and eurostat. Financial institutions processing ECB data are subject to EU financial regulation and often have explicit data residency requirements.

Clinical and pharmaceutical data. The EMA's R guidelines for clinical trial submissions make R the required language for regulatory submissions in the EU. Models built on patient data for regulatory purposes must be processed in GDPR-compliant environments. An R API serving predictions from clinical models should run on EU infrastructure.

GDPR and the eurostat package. The eurostat package by Leo Lahti (Finnish, University of Turku) and colleagues at Finnish and Belgian universities provides direct access to Eurostat data from R. It is one of the most-used packages in EU academic and policy research. Building APIs on top of eurostat data is a natural use case for R backends hosted in Europe.

The R Core Team's European Roots

R's technical foundations were laid by New Zealanders Ross Ihaka and Robert Gentleman, but the language has been developed and maintained by a global team with heavy European representation for over two decades.

The R Core Team today includes members from ETH Zürich, WU Wien, the University of Copenhagen, the University of Zurich, and other EU institutions. The CRAN infrastructure, which makes R's package ecosystem work, is maintained largely from Vienna and other European universities. Rcpp, which makes R fast enough for production use, came from Germany.

When EU organisations choose R, they are choosing a language whose production infrastructure was significantly shaped by European researchers and developers. Deploying that infrastructure to European servers completes the picture.


Deploy your R Plumber API to EU infrastructure today. sota.io is the EU-native PaaS built for data science teams — GDPR-compliant, managed PostgreSQL, zero DevOps overhead. Your model runs in the same environment it was trained in, on servers that never leave Europe.

Get started with sota.io →