2026-04-02Β·8 min readΒ·sota.io team

Deploy LFE (Lisp Flavoured Erlang) to Europe β€” BEAM + Lisp on EU Infrastructure in 2026

The BEAM virtual machine has delivered 99.9999999% uptime β€” nine nines β€” on Ericsson's AXD301 telephone exchange switches for over two decades. That is 31 milliseconds of downtime per year. The machine that achieves this is not a miracle of hardware; it is the result of systematic software engineering: a process model where every computation runs in isolated lightweight processes, where supervision trees restart failed processes automatically, where code can be replaced in a running system without stopping it. The BEAM was designed for the requirements of Swedish national telecom infrastructure in the 1980s. It remains unmatched for fault tolerance in production today.

LFE β€” Lisp Flavoured Erlang β€” brings the full expressive power of a Lisp to this battle-tested runtime. Where Erlang uses its own syntax and Elixir uses a Ruby-inspired syntax, LFE uses Common Lisp-style s-expressions with hygienic macros. The result is a language where the macro system can transform any syntax into arbitrary code at compile time, running on a virtual machine designed for distributed, fault-tolerant, production systems.

LFE was created by Robert Virding πŸ‡ΈπŸ‡ͺ β€” one of the three engineers who built Erlang at Ericsson's Computer Science Laboratory in Stockholm in 1986. The language that runs Swedish and European telecom infrastructure has a Lisp running on top of it, built by the man who helped invent it.

Robert Virding and the Ericsson Origin

Robert Virding πŸ‡ΈπŸ‡ͺ is a Swedish computer scientist who worked at Ericsson's Computer Science Laboratory in Stockholm from the mid-1980s onward. In 1986, he joined Joe Armstrong πŸ‡¬πŸ‡§ and Mike Williams πŸ‡¬πŸ‡§ to build a new programming language for Ericsson's next-generation telecommunications switches. The problem they were solving was specific: existing languages β€” C, Prolog, PLEX β€” could not express the concurrency and fault-tolerance requirements of production telephone exchanges at scale. They needed something new.

The language they built was Erlang. Its design decisions were not theoretical preferences β€” they were engineering requirements extracted from real telecom failure modes. Processes were cheap and isolated so that a single failed call would not cascade into a system-wide outage. Message passing replaced shared memory so that processes could fail without corrupting each other's state. Supervision trees formalised the restart logic that operators had been implementing manually. Hot code loading allowed software updates to be applied to a running switch without disconnecting calls.

Ericsson released Erlang as open source in 1998. The AXD301 switch β€” running Erlang in production β€” achieved the nine-nines figure that became the BEAM's defining credential. Ericsson equipment built on this heritage now runs in mobile networks across Europe: Deutsche Telekom πŸ‡©πŸ‡ͺ, Telia πŸ‡ΈπŸ‡ͺ, Vodafone πŸ‡©πŸ‡ͺπŸ‡³πŸ‡±, Orange πŸ‡«πŸ‡·, Swisscom πŸ‡¨πŸ‡­.

Robert Virding began work on LFE in 2007. The motivation was direct: he had always been interested in Lisp, and the question was whether a well-designed Lisp could be made to feel natural on the BEAM β€” not a translation layer, but a first-class member of the BEAM language family. LFE compiles to the same BEAM bytecode as Erlang and Elixir. It interoperates with them at the module level without any foreign function interface overhead. An LFE process and an Erlang process can exchange messages directly.

What LFE Offers

LFE is not a Lisp with Erlang bolted on. It is Erlang with Lisp syntax and Lisp metaprogramming.

Hygienic macros. LFE's macro system follows Common Lisp conventions but with hygienic capture avoidance. Macros are compile-time transformations that operate on the s-expression syntax of the source code. You can define new control structures, new DSLs, new data transformation pipelines β€” all resolved at compile time to efficient BEAM bytecode. The macro system allows you to extend the language itself without any runtime cost.

Full Erlang pattern matching. Pattern matching in LFE works identically to Erlang β€” function clauses, case expressions, receive blocks β€” but expressed in Lisp syntax. Destructuring, guard clauses, and binary pattern matching are all available.

OTP supervision trees. The full OTP (Open Telecom Platform) library is available from LFE. You write gen_server, supervisor, gen_statem behaviours in LFE s-expression syntax. The supervision tree structure β€” the core mechanism of BEAM fault tolerance β€” is expressed in a notation that makes its recursive structure visually explicit.

Distributed BEAM nodes. LFE code can spawn processes on remote BEAM nodes, send and receive messages across the network, and participate in distributed OTP applications. The distribution is built into the runtime; no additional configuration is required beyond node naming and cookie authentication.

Hot code reloading. BEAM's code server maintains two versions of every module in memory simultaneously. LFE modules can be updated in a running system β€” new function clauses loaded, old ones purged β€” without stopping the system or disconnecting clients.

LFE Web Backend with Cowboy

The standard approach for LFE HTTP backends is Cowboy, the HTTP/1.1 and HTTP/2 server written in Erlang by LoΓ―c Hoguin πŸ‡«πŸ‡· (French developer, Nine Nines, France). Cowboy is used in production by WhatsApp, Klarna πŸ‡ΈπŸ‡ͺ, and dozens of EU financial technology companies.

A rebar.config for an LFE Cowboy application:

{erl_opts, [debug_info]}.
{deps, [
  {lfe, "2.1.2"},
  {cowboy, "2.10.0"},
  {epgsql, "4.7.0"}
]}.
{plugins, [rebar3_lfe]}.
{provider_hooks, [
  {pre, [{compile, {lfe, compile}}]}
]}.
{relx, [{release, {lfe_api, "0.1.0"}, [lfe_api, sasl]}]}.

The main application module in LFE:

;; src/lfe_api_app.lfe
(defmodule lfe_api_app
  (behaviour application)
  (export (start 2) (stop 1)))

(defun start (start-type start-args)
  (let* ((dispatch (cowboy_router:compile
                     (list (tuple '_
                       (list
                         (tuple "/health"       health_handler  (list))
                         (tuple "/api/items"    items_handler   (list))
                         (tuple "/api/items/:id" item_handler   (list)))))))
         (port (list_to_integer
                 (os:getenv "PORT" "8080"))))
    (cowboy:start_clear 'http
      (list (tuple port port))
      (map 'env (map 'dispatch dispatch)))
    (lfe_api_sup:start_link)))

(defun stop (state) 'ok)

A Cowboy handler in LFE showing pattern matching and PostgreSQL via epgsql:

;; src/items_handler.lfe
(defmodule items_handler
  (export (init 2) (allowed_methods 2) (content_types_provided 2)
          (list_items 2)))

(defun init (req state)
  (tuple 'cowboy_rest req state))

(defun allowed_methods (req state)
  (tuple (list <<"GET">>) req state))

(defun content_types_provided (req state)
  (tuple (list (tuple (tuple <<"application">> <<"json">> (list))
                      list_items))
         req state))

(defun list_items (req state)
  (let* ((db-url (os:getenv "DATABASE_URL"))
         (result (query-items db-url)))
    (case result
      ((tuple 'ok items)
       (let ((body (jsone:encode items)))
         (tuple body req state)))
      ((tuple 'error reason)
       (let ((body (jsone:encode (map "error" (list_to_binary
                                               (io_lib:format "~p" (list reason)))))))
         (tuple (cowboy_req:reply 500 (map) body req) req state))))))

(defun query-items (db-url)
  (case (epgsql:connect db-url)
    ((tuple 'ok conn)
     (case (epgsql:equery conn "SELECT id, name, created_at FROM items ORDER BY created_at DESC LIMIT 50" (list))
       ((tuple 'ok _cols rows)
        (epgsql:close conn)
        (tuple 'ok (lists:map #'row-to-map/1 rows)))
       ((tuple 'error reason)
        (epgsql:close conn)
        (tuple 'error reason))))
    ((tuple 'error reason) (tuple 'error reason))))

(defun row-to-map
  ([(tuple id name created-at)]
   (map "id" id "name" name "created_at" (calendar:system_time_to_universal_time
                                           (erlang:system_time 'second) 'second))))

A supervision tree in LFE β€” the structure that gives OTP applications their fault tolerance:

;; src/lfe_api_sup.lfe
(defmodule lfe_api_sup
  (behaviour supervisor)
  (export (start_link 0) (init 1)))

(defun start_link ()
  (supervisor:start_link (tuple 'local 'lfe_api_sup)
                         'lfe_api_sup (list)))

(defun init (_args)
  (let ((worker-spec
          (map 'id       'lfe_api_worker
               'start    (tuple 'lfe_api_worker 'start_link (list))
               'restart  'permanent
               'shutdown 5000
               'type     'worker
               'modules  (list 'lfe_api_worker))))
    (tuple 'ok
           (tuple (map 'strategy  'one_for_one
                       'intensity 10
                       'period    60)
                  (list worker-spec)))))

The one_for_one strategy means: if the worker process crashes, restart only that process β€” the supervisor and other siblings continue running. This is the operational model that achieves nine-nines uptime.

Dockerfile for sota.io Deployment

FROM erlang:26-alpine AS builder

# Install rebar3 and LFE
RUN apk add --no-cache git curl
RUN curl -fsSL https://s3.amazonaws.com/rebar3/rebar3 -o /usr/local/bin/rebar3 \
    && chmod +x /usr/local/bin/rebar3

WORKDIR /app
COPY rebar.config rebar.lock ./
COPY src/ ./src/

# Fetch deps and compile
RUN rebar3 get-deps
RUN rebar3 compile
RUN rebar3 release

# ---- runtime stage ----
FROM erlang:26-alpine

RUN apk add --no-cache libstdc++ openssl

WORKDIR /app
COPY --from=builder /app/_build/default/rel/lfe_api ./

EXPOSE 8080

CMD ["./bin/lfe_api", "foreground"]

The multi-stage build keeps the final image lean β€” the Erlang runtime is approximately 20MB. The foreground flag runs the OTP release in the foreground with logs to stdout, which is what sota.io's log aggregator expects.

EU Compliance: NIS2, GDPR, and BEAM Fault Isolation

NIS2 Directive (EU 2022/2555) β€” Critical Infrastructure Resilience. The NIS2 Directive requires organisations in essential sectors (energy, transport, finance, health, digital infrastructure) to implement measures ensuring service continuity and rapid recovery. BEAM's supervision tree model provides a formal, automated recovery mechanism: when a component fails, the supervisor restarts it according to a defined strategy. The restart intensity configuration (restarts per second) makes the failure handling policy explicit and auditable β€” directly addressing NIS2 Article 21's requirement for documented incident recovery capabilities.

GDPR Article 5(1)(f) β€” Integrity and Confidentiality. The BEAM process model enforces data isolation by construction. Each BEAM process has its own private heap. Processes communicate only through message passing β€” there is no shared mutable memory. A process that crashes cannot corrupt another process's data. For systems handling personal data, this architecture means a failure in one processing pipeline cannot leak data into another. The isolation is not a coding convention; it is enforced by the runtime.

GDPR Article 25 β€” Data Protection by Design. OTP's gen_server behaviour structures state management around explicit state transitions. The state of every server process is explicit, typed, and owned by that process. There are no global variables and no ambient state that could silently accumulate personal data outside the intended data flow. The architecture enforces minimisation by making all state explicit.

GDPR Article 32 β€” Security of Processing. Hot code reloading means security patches can be applied to production systems without service interruption. In a conventional deployment, a critical CVE requires a restart β€” a window during which the vulnerability is either exposed (no restart yet) or users experience downtime. BEAM's code server allows a patched module to be loaded while the system continues running, closing the vulnerability without an outage.

EU AI Act Article 9 β€” Risk Management. For AI systems deployed under EU AI Act obligations, supervision trees provide a documented, verifiable risk management mechanism. Each component of the AI system can be wrapped in an OTP supervisor with defined restart policies. If a model inference process hangs or crashes, the supervisor restarts it within milliseconds. The fault tree is explicit in the code, auditable without runtime instrumentation.

Sweden and the BEAM's European Foundation

The BEAM ecosystem has a distinctly European character at its core. Ericsson πŸ‡ΈπŸ‡ͺ created Erlang and the BEAM VM and continues to fund their development. Nine Nines πŸ‡«πŸ‡· (LoΓ―c Hoguin's company) maintains Cowboy, the HTTP server that powers much of the BEAM web ecosystem. Klarna πŸ‡ΈπŸ‡ͺ (Stockholm) runs one of Europe's largest fintech platforms on Erlang and Elixir. WhatsApp, before its 2014 acquisition, ran with 50 engineers serving 900 million users β€” on Erlang. The protocol that handles message delivery in European banking: Erlang. The real-time session management in EU telecoms: BEAM nodes.

Robert Virding continues to maintain LFE and speaks at conferences across Europe about Lisp, distributed systems, and the BEAM. He brings to LFE not just the syntactic tradition of Lisp β€” going back to John McCarthy πŸ‡ΊπŸ‡Έ (MIT, 1958) β€” but the systems engineering tradition of Ericsson: build for failure, isolate processes, restart automatically, keep the system running.

LFE is not a widely-used language. It is a niche language used by people who want Lisp expressiveness and BEAM reliability simultaneously, and who are willing to accept the learning curve that comes with both. But its production credentials are Erlang's production credentials β€” and those are unmatched.

Deploy LFE to Europe with sota.io

sota.io runs on Hetzner infrastructure in Germany β€” the same country where Ericsson's European R&D sites operate. Your LFE application deploys from a single Dockerfile.

Prerequisites:

Deploy steps:

  1. Create a new project in the sota.io dashboard. Select "Dockerfile" as the build method.

  2. Set environment variables:

    • DATABASE_URL: your PostgreSQL connection string (sota.io provides managed PostgreSQL)
    • PORT: 8080 (set automatically by sota.io)
    • ERL_FLAGS: -sname lfe_api for OTP node naming
  3. Push your code. sota.io builds from the Dockerfile in your repository root. The multi-stage build handles the LFE compilation.

  4. Configure health checks. sota.io expects a /health endpoint returning 200. The Cowboy handler above provides this.

  5. Scale horizontally. Each LFE instance is a fully independent OTP node. For stateful applications using ETS or Mnesia, configure sticky sessions or use the BEAM's built-in distribution protocol to synchronise state across nodes.

Managed PostgreSQL. sota.io provisions a PostgreSQL instance in the same Hetzner region as your application. The DATABASE_URL connection string is injected as an environment variable. The epgsql driver handles connection pooling via poolboy (another Erlang library β€” devinus πŸ‡©πŸ‡ͺ contributor, widely used in EU fintech).

The nine-nines figure comes from Ericsson's production deployment. Your application may not serve national telecom infrastructure. But the supervision model, the process isolation, the hot code loading, and the message-passing architecture that produced that figure are available in your Dockerfile, running on German infrastructure, with GDPR-compliant PostgreSQL.

Robert Virding put Lisp on the BEAM because he could. Because the BEAM is expressive enough to host any language, and because the combination β€” Lisp macros with Erlang fault tolerance β€” produces a development experience that no other platform offers. Deploy it to sota.io, and the infrastructure that European telecom was built on runs your API.


Deploy any BEAM language to Europe with sota.io. See also: Deploy Erlang to Europe β†’, Deploy Elixir & Phoenix to Europe β†’, Deploy Gleam to Europe β†’