The Stateful Edge & WebSocket Gateway. Zero Cloud Bloat.
A minimalist approach to realtime infrastructure and API routing, built strictly on the principle of "Dumb Pipes, Smart Endpoints."
This open-source project demonstrates that you don't need complex Kubernetes meshes, managed API Gateways, or expensive SaaS providers like Pusher to handle enterprise-scale traffic. Capable of sustaining millions of concurrent connections on a single commodity server (e.g., Hetzner), BeamGate is more than just a WebSocket server—it is a robust, highly extensible edge router. It splits responsibilities exactly where they belong.
Built on the legendary Erlang/OTP VM, BeamGate acts as an impenetrable, stateful edge. It handles reverse proxying, HTTP routing, long-lived connection lifecycle, protocol framing, OOM protection, and lightning-fast PubSub distribution. Meanwhile, your standard, stateless application (e.g., PHP, Python, Go) serves as the single source of truth, retaining full native control over business logic, authentication, and state mutations.
High performance, uncompromising security, and absolutely zero vendor lock-in. Use it as a drop-in alternative to expensive cloud infrastructure, or as a rock-solid foundation for your own custom edge logic.
- The Unified Edge: Seamlessly handles both stateless HTTP reverse proxying and stateful WebSocket connections on the same port.
- Dumb Pipes, Smart Endpoints: Keeps complex state management out of the edge proxy. BeamGate handles pure connectivity and framing, deferring all business logic and auth to your stateless HTTP backends via Webhooks.
- A Hackable Foundation: Built with clean Erlang OTP principles. It's not a black box—it's a starting point designed to be easily extended with your own custom routing, protocols, or edge logic.
- Strict UNIX Philosophy: Components do exactly one job effectively (TCP acceptor, router, protocol decoder, webhook client).
- Secure by Design: Built-in payload boundaries on WebSocket frame ingestion to defeat OOM DoS, secure handshake buffering, and strict HTTP header sanitization (preventing spoofing).
- Start your stateless Webhook handler on port
8000. - Start BeamGate:
$ rebar3 compile
$ rebar3 shellThis guide covers deploying BeamGate and a PHP application to a Hetzner Cloud VM or dedicated root server, tuning it for massive concurrency (1,000,000+ connections), and setting up reverse proxies via Nginx.
To handle massive amounts of sockets (1M+ concurrent WebSockets), the system requires significant OS-level tuning. The network stack, file descriptors, and memory allocation must be optimized.
Server Requirements for 1M Connections:
- CPU: 8+ Cores minimum (Erlang utilizes all cores efficiently).
- RAM: 32GB RAM minimum, ideally 64GB+ (Each socket and its associated Erlang process takes a small amount of memory, typically around 5-15KB per idle connection. 1M connections require roughly 10-15GB just for sockets, plus OS and PHP/HTTP overhead).
- Network: 1 Gbps up/down minimum (Hetzner Standard is typically 1G, 10G on dedicated root servers).
- OS: Ubuntu 22.04 / 24.04 LTS or Debian 12.
What we need to install:
- Erlang/OTP 26+ and
rebar3. - PHP 8.x + PHP-FPM (for the backend application logic).
- Nginx (for TLS termination, routing WS to BeamGate and HTTP to PHP-FPM).
Before starting the applications, configure the Linux kernel to support over 1,000,000 open files/sockets.
Edit /etc/sysctl.conf and add:
# Max open files per system
fs.file-max = 2500000
# Increase local port range
net.ipv4.ip_local_port_range = 1024 65535
# Increase TCP max memory
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
# Connection tracking and backlogs
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.tcp_tw_reuse = 1Apply with sudo sysctl -p.
Edit /etc/security/limits.conf for the users running Nginx and BeamGate to allow individual processes to use millions of file descriptors:
* soft nofile 1048576
* hard nofile 1048576
root soft nofile 1048576
root hard nofile 1048576You can compile BeamGate directly on the Hetzner server or build an Erlang release for cross-environment deployment.
Option A: Direct Compilation on Server
- Install Erlang/OTP:
sudo apt install erlang rebar3 - Clone the repository:
git clone https://github.com/your-org/beamgate.git /opt/beamgate - Compile:
cd /opt/beamgate rebar3 compile
Option B: Creating a Self-Contained Release (Recommended for cross-compilation)
To cross-compile or create an artifact that doesn't need Erlang installed on the target machine, assure your rebar.config has a relx block:
{relx, [{release, {beamgate, "0.1.0"}, [beamgate_core, sasl]},
{mode, prod},
{extended_start_script, true}]}.Then run rebar3 as prod release or rebar3 as prod tar in an environment matching the target server's architecture (e.g., using an Ubuntu 22.04 Docker container if working from macOS). Copy the resulting artifact from _build/prod/rel/beamgate to /opt/beamgate on your Hetzner server.
We need two systemd services: one for the PHP application daemon/FPM, and one for BeamGate.
PHP-FPM is normally handled via the distribution's package: systemctl enable --now php8.2-fpm.
BeamGate Systemd Service (/etc/systemd/system/beamgate.service)
[Unit]
Description=BeamGate Stateful Edge Server
After=network.target
[Service]
Type=simple
User=beamgate
Group=beamgate
WorkingDirectory=/opt/beamgate
# Enable Erlang to handle 1,000,000+ ports/sockets
Environment="ERL_MAX_PORTS=1500000"
# Run command (if running via shell over compiled source):
ExecStart=/usr/bin/rebar3 shell --config config/sys.config
# If running as a release:
# ExecStart=/opt/beamgate/bin/beamgate foreground
Restart=on-failure
RestartSec=5
# Allow systemd to open 1M files for this service
LimitNOFILE=1048576
[Install]
WantedBy=multi-user.targetEnable and start the service: sudo systemctl daemon-reload && sudo systemctl enable --now beamgate
To handle HTTP API traffic, terminate SSL, and route WebSockets to BeamGate reliably under extreme load, Nginx must be tuned alongside the OS.
Nginx Main Config (/etc/nginx/nginx.conf)
user www-data;
# Set worker processes to match CPU cores
worker_processes auto;
# Allow workers to keep open a massive amount of file descriptors
worker_rlimit_nofile 1048576;
pid /run/nginx.pid;
events {
# 1 Million total connections = worker_processes * worker_connections
# Example: If your Hetzner server has 8 cores -> ~130,000 per worker
worker_connections 130000;
multi_accept on;
use epoll;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Consider turning off access logging for WS endpoints under heavy load
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log crit;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}Nginx Server Block (/etc/nginx/sites-available/beamgate)
# Upstream for BeamGate
upstream beamgate {
server 127.0.0.1:8080; # Assuming BeamGate TCP handles WS here
keepalive 1024;
}
# Upstream for PHP Application
upstream php_backend {
server unix:/run/php/php8.2-fpm.sock;
}
server {
listen 80;
listen [::]:80;
server_name api.yourdomain.com;
# Route All Public Traffic (WS & HTTP) to BeamGate
location / {
proxy_pass http://beamgate;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 3600;
proxy_send_timeout 3600;
# Disable buffering to allow immediate duplex data flow
proxy_buffering off;
}
}
# Internal Webhook Server - Strictly for BeamGate's sys.config endpoints
# Denies access from the outside world
server {
listen 127.0.0.1:8000;
server_name localhost;
root /opt/beamgate/php_app;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass php_backend;
}
}Because BeamGate is built on the Erlang/OTP VM, true experts do not restart the daemon and drop 1,000,000 active sockets when deploying a new version. Instead, Erlang supports "Hot Code Swapping"—replacing code in memory while processes continue to run.
Method 1: Manual Hot Code Reloading (For fast, small patches)
If you compiled directly on the server (Option A) and just need to patch a few modules (e.g., beamgate_router.erl):
- Navigate to the directory and pull changes:
cd /opt/beamgate && git pull - Recompile the updated code:
rebar3 compile - Connect into the running BeamGate node. If you used
rebar3 shell, connect a remote shell (assumes you booted it with a name and cookie, e.g.,-name node@127.0.0.1 -setcookie mycookie). - Inside the Erlang shell, reload the specific module:
1> l(beamgate_router). {module, beamgate_router}
The code is instantly updated. All active sockets remain connected and will immediately execute the new logic on their next message.
Method 2: True OTP Release Upgrades (Relups) For fully automated, versioned, zero-downtime deployments without manual shell intervention (Option B):
- Define the upgrade instructions in an
.appupfile (telling OTP how to migrate internal state between your versions). - Build a release upgrade (
relup) tarball using absolute versioning on your build machine:rebar3 as prod release rebar3 as prod relup -n beamgate -v "0.2.0" -upfrom "0.1.0" rebar3 as prod tar
- Copy the resulting
beamgate-0.2.0.tar.gzto the target server into the/opt/beamgate/releases/directory. - Tell the running system to unpack and upgrade to the new version gracefully:
/opt/beamgate/bin/beamgate upgrade "0.2.0"
The Erlang release handler unpacks the new version, automatically suspends active processes for a fraction of a millisecond, hot-swaps the loaded .beam modules, runs your state transformations, and resumes execution. Your users—and those 1 Million sockets—will experience absolutely zero downtime.