Traffic Overview

Real-time metrics from the data plane

mode: compact 0d
Active connections
0
sessions in flight right now
Requests / second
0
inbound throughput
Accepted
0
total connections accepted
Failed / rejected
0 / 0
errors & maxconn exhausted
Bytes in / out
0 B / 0 B
total traffic both directions
Log dropped / error
0 / 0
queue & disk writer

Traffic — last 60 points

Req/sec Active conns

Log mode

Full captures headers, cookies & bodies. Automatic escalation on errors stays active in compact mode.

Frontends

    Backend pool

    active health checks · rise/fall
    BackendServerStatus WeightSticky ID ActiveTotal ErrorBytes in/out Check

    Log Viewer

    Live tail · last 1000 lines in memory · same file as disk log

    mode: compact 0 lines 0 dropped

    Access log

    Waiting for traffic…
    click a line to expand the raw record live

    Resources & Capacity

    Server + LB process usage · analytics · upgrade advice

    healthy — never

    Alerts & capacity advice

    0 active
      Recent events

        Server

        —
        CPU
        —%
        load avg —
        Memory
        —%
        — of —
        Disk (log volume)
        —% free
        — available
        Network I/O
        —↓ —↑
        rx / tx per second (excl. loopback)

        lbrust process

        pid —
        Process CPU
        —%
        of total system capacity
        RSS memory
        —
        resident set size
        Threads
        —
        worker + helper threads
        Open files
        —
        vs ulimit -n

        Connection capacity

        active vs maxconn
        0%
        Active0
        maxconn0
        Rejected (total)0
        Rejected / min0
        Headroom100%

        Analytics

        CPU % Memory % Connections Bandwidth
        CPU / Memory (%)
        Connections vs maxconn
        Bandwidth (B/s)
        History accumulates while lbrust runs — 1 hour at full resolution, 7 days at 1-minute resolution.

        Config Editor

        Edit live · validate · apply without restart

        lbrust.toml TOML
        Tab indent Ctrl+/ comment Ctrl+Space autocompletion Ctrl+F cari Esc leave editor
        Perubahan bind/mode frontend, [log.file] dan [admin] butuh restart proses — sisanya diterapkan langsung.

        Documentation

        Complete lbrust configuration guide — from basics to SSL & tuning

        Quickstart

        Build, set an admin password, then run — three steps.

        # 1) build (or use an existing release binary)
        cargo build --release
        
        # 2) create the admin password hash → paste it into the config
        ./target/release/lbrust hash-password
        #   output: $argon2id$...   →  put it in [admin] password_hash
        
        # 3) validate, then run
        ./target/release/lbrust -c config/example.toml --test
        ./target/release/lbrust -c config/example.toml run
        GUI: open http://127.0.0.1:9464 and sign in with that password. Monitoring, Settings (config editor), Docs, and the Restart button live there.
        CommandPurpose
        lbrust runRun the load balancer (default)
        lbrust checkValidate config then exit (exit 2 if invalid)
        lbrust hash-passwordCreate the argon2id hash for the admin
        -c FILEConfig path (default: lbrust.toml)
        -t / --testSame as check
        --log-level LEVELOverride log level: error|warn|info|debug|trace
        # signals
        SIGHUP          # reload config from file (no restart)
        SIGINT/SIGTERM  # graceful shutdown + connection drain

        Config format

        The config uses TOML with HAProxy-style concepts 1:1 — it is not HAProxy syntax. Each section is a table / array of tables:

        [global]          # process behavior: workers, maxconn, drain
        [defaults]        # defaults for frontends/backends
        [[frontend]]      # many allowed — each bind listens for traffic
        [[backend]]       # many allowed — server pool + balancer
          [[backend.server]]  # a server inside a backend
        [log]             # mode, level, escalation
        [log.file]        # THE log file: compact & full records, rotation + retention
        [log.capture]     # capture parameters for full mode
        [monitor]         # resource sampling, capacity alerts, webhook (hot-reload)
        [monitor.thresholds]  # CPU/mem/maxconn/disk/fd alert levels
        [admin]           # admin GUI/API

        Durations and sizes use flexible strings:

        TypeExamples
        Duration"500ms" "5s" "2m" "1h30m" "14d"
        Size"512MB" "2GB" "1024" (bytes)
        Validation always shows the complete list of errors at once — both via lbrust --test and the Validate button in the GUI.

        [global] & [defaults]

        [global]
        workers = 0            # 0 = automatic (use physical core count)
        maxconn = 200000       # total active connection cap (admission control)
        backlog = 4096         # listen queue — match it with somaxconn
        drain_timeout = "30s"  # time to drain active conns on shutdown/restart
        
        [defaults]
        mode = "tcp"                 # "tcp" | "http" (http = L7: rules, cookie sticky)
        balance = "roundrobin"       # roundrobin | leastconn | source
        timeout_connect = "5s"       # batas waktu connect ke backend
        timeout_client  = "60s"      # idle timeout, client side
        timeout_server  = "60s"      # idle timeout, backend side
        retries = 1                  # retry another server on connect failure
        KeyDefaultNotes
        workers0 (auto)Thread-per-core, pinned, SO_REUSEPORT
        maxconn100.000Exceeding it → new connections are rejected (not queued)
        backlog4096Raise net.core.somaxconn on Linux
        retries0 (maks 10)The failed server is excluded during retries
        balanceroundrobinSmooth WRR (nginx algorithm) — even spread despite unequal weights

        Balance algorithms:

        • roundrobin — smooth weighted round-robin (nginx)
        • leastconn — pick the server with the fewest active connections (good for long-lived traffic)
        • source — rendezvous hashing (HRW) over the client IP: deterministic, stable across instances

        [[frontend]]

        [[frontend]]
        name = "app_in"
        bind = ["*:9000"]            # "*:PORT" | "IP:PORT" | "[::]:PORT"
        default_backend = "app"
        # mode = "tcp"               # override defaults.mode
        # forceclose = true          # force Connection: close (needed for per-request rules)
        # rule = [ ... ]             # see the Routing rules section
        
        # TLS termination — see the SSL/TLS section
        # [frontend.tls]
        # crt = "/etc/lbrust/fullchain.pem"
        # key = "/etc/lbrust/privkey.pem"
        KeyRequiredDescription
        name✓Unique — used in logs & monitoring
        bind✓List of listen addresses; one frontend can have many binds
        default_backend✓Target backend when no rule matches
        mode—Inherits defaults.mode
        tls—Certificate block — see the SSL/TLS section

        [[backend]]

        [[backend]]
        name = "app"
        balance = "roundrobin"       # override defaults.balance
        # sticky = { type = "source_ip", hash = "xxhash64" }
        # sticky = { type = "cookie", name = "SRV", ttl = "30m",
        #            httponly = true, secure = true, path = "/" }
        
          [[backend.server]]
          addr = "10.0.0.1:8080"     # IP:port atau hostname (DNS di-refresh tiap check)
          weight = 2                 # 1..=100 (default 1)
          maxconn = 10000            # 0 = unlimited per server
          # cookie = "s1"            # sticky id (default: addr)
          # disabled = true          # present in config but disabled
        
          [[backend.server]]
          addr = "10.0.0.2:8080"
          check = { type = "tcp", interval = "2s", timeout = "1s",
                    rise = 2, fall = 3 }
          # check = { type = "http", path = "/healthz", ... }
        
          # TLS ke backend (HAProxy: `server ... ssl ...`)
          # ssl = { verify = false }                       # verify none — self-signed internal
          # ssl = { verify = true, cafile = "/etc/ca.pem" } # verifikasi penuh + CA bundle
        Backend TLS: set ssl = { ... } on a server and every connection to it (proxy traffic and health checks) is wrapped in TLS. verify = false accepts any certificate — the same semantics as HAProxy's ssl verify none, suitable for internal services like CockroachDB. verify = true requires cafile (PEM CA bundle) and validates the full chain. ALPN offered is http/1.1.
        DNS: when addr is a hostname, resolution is refreshed automatically every health check cycle. Resolution failure → the server is considered down until a check restores it.

        Routing rules (ACL subset)

        Rules pick a backend based on request conditions — they only apply in mode = "http" (tcp mode has no concept of a request).

        [[frontend]]
        name = "web"
        bind = ["*:80"]
        default_backend = "app"
        
        rule = [
          { if = "path_beg(\"/api\")",   backend = "api" },
          { if = "path_end(\".png\")",   backend = "static" },
          { if = "host(\"example.com\")", backend = "vhost" },
          { if = "method(\"POST\")",     backend = "write" },
          { if = "hdr(\"x-beta\", \"1\")", backend = "beta" },
          { if = "not path_beg(\"/admin\")", backend = "public" },
        ]
        FunctionExampleMatches when…
        path_begpath_beg("/api")path starts with the string
        path_endpath_end(".jpg")path ends with the string
        pathpath("/health")path equals exactly
        hosthost("example.com")Host header matches (case-insensitive)
        methodmethod("POST")HTTP method (uppercased first)
        hdrhdr("x-api-key", "abc")header with an exact value
        not …not path_beg("/x")negation of a condition
        Status: implemented. Rules are evaluated per request in mode = "http" — first match wins, no match falls back to default_backend. Rules on a tcp frontend are rejected (tcp has no request to inspect).

        Sticky sessions

        Two modes — set them inside [[backend]]:

        # 1) Cookie-based (http mode) — the LB hands the client a cookie
        sticky = { type = "cookie", name = "SRV", ttl = "30m",
                   httponly = true, secure = true, path = "/" }
        
        # 2) Source-IP hashing (tcp/http mode) — client = source IP
        sticky = { type = "source_ip", hash = "xxhash64" }
        Aspectsource_ipcookie
        Selection basisClient IP (rendezvous/HRW)Cookie inserted by the LB
        Modetcp & httphttp only
        StabilitasServer dies → only clients on that IP moveServer dies → cookie clients are remapped
        Across instances✓ consistent (deterministic hash)✓ (as long as the cookie algorithm matches)
        KelemahanMany clients behind NAT pile onto one serverClients without cookies (curl) do not stick
        HRW (Rendezvous): score = hash(server_id, client_ip); the highest-scoring server wins. When one server disappears, only the clients mapped to it move — minimal disruption.

        Health checks

        Active — the LB proactively tests servers every interval:

        [[backend.server]]
        addr = "10.0.0.1:8080"
        check = { type = "tcp", interval = "2s", timeout = "1s",
                  rise = 2, fall = 3 }
        
        # HTTP check — 2xx/3xx responses are considered healthy
        check = { type = "http", path = "/healthz",
                  interval = "2s", timeout = "1s", rise = 2, fall = 3 }
        KeyDefaultMeaning
        typetcptcp (connect only) | http (GET + status check)
        interval2sGap between checks (min 100ms)
        timeout2sTime limit for one check
        rise2Consecutive successful checks → server marked UP
        fall3Consecutive failed checks → server marked DOWN

        Passive — no config needed, derived from real traffic:

        • Connect failure from the proxy → the server counts as failed, then exponential backoff 5s → 10s → 20s → … max 60s (excluded from selection during backoff)
        • Successful connect → failure counter resets
        • The log records backend UP / backend DOWN when rise/fall is reached

        Logging & retention

        [log]
        level = "info"               # error|warn|info|debug|trace
        mode = "compact"             # compact | full (togglable from the GUI at runtime)
        escalate_on_error = true     # errors are logged in FULL detail, even in compact mode
        error_status_min = 500
        format = "json"              # json | text
        
        [log.file]                   # ONE file for both compact and full records
        enabled = true
        path = "logs/access.log"
        rotate = "daily"             # hourly | daily | size
        # max_size = "500MB"         # used when rotate = "size"
        retention_days = 14          # delete archives older than this…
        retention_size = "2GB"       # …AND/OR when the total exceeds this
        compress = true              # gzip archives
        queue_capacity = 8192        # queue full → drop + counter, proxy keeps running
        
        [log.capture]                # applies when a FULL record is written
        max_body_bytes = "8KB"       # body capture limit
        capture_request_body = true
        capture_response_body = false
        redact_headers = ["authorization", "proxy-authorization", "x-api-key"]
        body_content_types = ["application/json",
                              "application/x-www-form-urlencoded", "text/*"]
        sample_rate = 1.0            # 0..1 — sampling rate for full mode
        One file, two detail levels. mode = "compact" writes one short line per request; mode = "full" writes one detailed line (headers, cookies, bodies) for each request instead — never both. Escalated errors always get the full record. The old [log.full] section is still accepted for compatibility but ignored.
        BehaviorExplanation
        Non-blockingProducers use a bounded queue — full → drop + counter (see Log dropped on the dashboard). The data plane never waits on disk.
        Rotasihourly/daily/size; archives are named access-YYYYMMDD-HHMMSS.log.gz
        Retensiretention_days AND retention_size — whichever hits first triggers deletion; swept every 10 min + at rotation
        EscalasiEven in compact mode, failed requests (connect failure/timeout, code ≥ error_status_min) are written with full detail (headers + payload preview)
        Mode runtimeToggle Compact/Full from the dashboard or the Logs page — applied instantly and saved to the config file, so it survives restart
        Log ViewerThe Logs page tails the last 1000 lines live (from memory — works even if file.enabled = false), with filter, follow and click-to-expand raw JSON

        Resources & alerts

        The Resources page samples the server (CPU, RAM, disk, network), the lbrust process (CPU, RSS, threads, open files vs ulimit) and the data plane (active/maxconn, rejected rate) — then evaluates capacity alerts and gives upgrade advice.

        [monitor]
        enabled = true
        interval = "5s"              # sampling rate (min 500ms) — also the chart resolution
        debounce = 3                 # consecutive samples before an alert fires/resolve
                                     # (anti-flap: 3 × 5s = 15s sustained)
        
        [monitor.thresholds]         # all percent 0..=100 — customize freely
        cpu_warn = 80                # system CPU ≥ 80% → warn, ≥ cpu_crit → critical
        cpu_crit = 95
        mem_warn = 85                # used memory %
        mem_crit = 95
        maxconn_warn = 80            # active connections as % of global.maxconn
        maxconn_crit = 95
        disk_warn = 10               # log volume FREE space ≤ 10% → warn (inverted!)
        disk_crit = 5                # …≤ 5% → critical
        fd_warn = 80                 # open files as % of ulimit -n
        fd_crit = 95
        reject_warn = 60             # rejected connections per MINUTE (not percent)
        reject_crit = 600
        
        [monitor.webhook]            # POST JSON on alert fire/resolve
        enabled = false
        url = "https://hooks.slack.com/services/XXX"   # http:// or https://
        min_interval = "5m"          # cooldown per alert (escalation always sends)
        timeout = "5s"
        Everything is hot-reloadable. Thresholds, interval, debounce and the webhook are re-read from the config on every sample — apply changes from Settings (or SIGHUP) and they take effect within one interval, no restart.
        AlertFires when…Suggested action
        cpusystem CPU sustained ≥ warn/critadd vCPU or scale out to more LB nodes
        memmemory used ≥ warn/critadd RAM; reduce log capture buffers
        maxconnactive/maxconn ≥ warn/critraise global.maxconn (+ ulimit) or add an LB node
        disklog volume free ≤ warn/critshorten retention or move [log.file]
        fdopen files ≥ % of ulimitraise LimitNOFILE / ulimit -n
        rejectrejected/min ≥ warn/critmaxconn full — raise it or add capacity

        Webhook payload

        One JSON body works for Slack (text), Discord (content) and generic receivers:

        {
          "text": "[lbrust] crit maxconn: Connections CRITICAL: 96.2% of maxconn (96200/100000) — …",
          "content": "…same…",
          "source": "lbrust",
          "event": "fire",            // or "resolve"
          "level": "crit",            // warn | crit | ok
          "alert": "maxconn",
          "message": "…", "advice": "…",
          "value": 96.2, "threshold": 95,
          "ts": 1762500000, "host": "lb-01"
        }

        Analytics & history

        • 1 hour at full sampling resolution (fine ring buffer)
        • 7 days at 1-minute resolution (coarse ring) — 24h view filters to the last day
        • Charts: CPU/memory %, connections vs maxconn, bandwidth rx/tx
        • Capacity advice: peaks of the last hour + headroom percentage
        • History lives in memory only (lost on restart) — export via Prometheus for long-term storage
        # prometheus resource metrics
        lb_sys_cpu_percent  lb_sys_mem_used_bytes  lb_proc_rss_bytes
        lb_proc_open_fds    lb_proc_fd_limit       lb_maxconn_utilization_percent
        lb_alerts_active    lb_alert{id="cpu",level="warn"} 1

        SSL / TLS

        Native TLS termination is built in. Set [frontend.tls] on an http frontend: crt accepts a combined PEM file or a directory of PEMs (HAProxy style), with multi-SNI selection — exact SAN match → wildcard *.domain → first cert as default. Patterns A/B below remain valid alternatives (external terminator, end-to-end passthrough).

        Pattern A — TLS terminator in front of lbrust (recommended)

        Encryption is terminated in a separate process (nginx/caddy/haproxy); lbrust stays L4 behind it. The most battle-tested option for production.

                internet ──TLS──▶ [terminator :443] ──HTTP/TCP──▶ [lbrust :9000] ──▶ backend
        # nginx — TLS terminator in front of lbrust
        server {
            listen 443 ssl http2;
            server_name example.com;
        
            ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
            ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
        
            location / {
                proxy_pass http://127.0.0.1:9000;   # ← lbrust bind
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
            }
        }
        
        # redirect HTTP → HTTPS
        server {
            listen 80;
            server_name example.com;
            return 301 https://$host$request_uri;
        }
        # free certificates with Let's Encrypt (certbot)
        sudo certbot --nginx -d example.com
        # or standalone if nginx is not running yet:
        sudo certbot certonly --standalone -d example.com
        # auto-renewal (enabled by default): certbot renew --dry-run

        Pattern B — end-to-end TLS (lbrust just passes bytes through)

        The connection stays encrypted from client to backend; lbrust forwards bytes as-is (tcp mode). Nobody reads the payload — ideal when the backend manages its own TLS.

                internet ──TLS──▶ [lbrust :443, mode tcp] ──TLS──▶ [backend TLS sendiri]
        [[frontend]]
        name = "tls_passthrough"
        bind = ["*:443"]
        default_backend = "secure_app"   # backend must terminate TLS itself

        Native TLS termination + multi-SNI

        Certificate selection per connection: exact SAN → wildcard → first cert (default).

        [[frontend]]
        name = "web"
        bind = ["*:443"]
        default_backend = "app"
        
        [frontend.tls]
        crt = "/etc/lbrust/fullchain.pem"   # cert chain + key (combined, HAProxy style)
        # key = "/etc/lbrust/privkey.pem"   # optional if the .crt file already includes the key
        # combine cert + key (HAProxy `crt` style)
        cat fullchain.pem privkey.pem > /etc/lbrust/fullchain.pem
        chmod 600 /etc/lbrust/fullchain.pem

        TLS security checklist

        • Use TLS ≥ 1.2 (ideally 1.3 only) with modern cipher suites
        • Enable HSTS once HTTPS is stable: Strict-Transport-Security: max-age=31536000; includeSubDomains
        • Secure the key: root-owned, mode 600, never in a repo
        • Watch expiry — certbot warns 30 days before it lapses
        • In lbrust: set secure = true on the sticky cookie once behind HTTPS

        Reload vs restart

        Three ways to apply changes:

        MethodScopeWhen
        Apply (reload)No connection dropsSettings button / SIGHUP
        RestartEverything, including restart-only keysRestart button in Settings (drain → re-exec)
        Start ulang manualEverythingsystemctl restart lbrust / run the binary again

        Hot-reloadable while running

        • Backends, servers, weights, balance, sticky
        • Frontend rules, default_backend
        • Timeout, retries, drain_timeout
        • Health checks (old checkers stop, new ones start — no duplicates)
        • Compact/full log mode (also written to the config file, so it survives restart)
        • [monitor] — thresholds, interval, debounce & webhook (re-read every sample)

        Requires a restart

        • bind / mode on a frontend — listeners must be recreated
        • [log.file] — path & rotation open at startup
        • [admin] — bind & admin password
        • [global] workers/maxconn
        Applying a restart-only change is rejected with a clear message ("requires a process restart") — use the Restart button. Restart drains first (active connections get time to finish), then re-execs the binary with the latest config from file.

        Admin & security

        [admin]
        enabled = true
        bind = "127.0.0.1:9464"      # localhost only — never expose it directly
        session_ttl = "8h"
        password_hash = "$argon2id$v=19$m=19456,t=2,p=1$..."  # dari `lbrust hash-password`
        login_max_attempts = 5       # failed attempts…
        login_window = "60s"         # …within this window → blocked
        LayerMechanism
        Passwordargon2id — plaintext is never stored
        Session256-bit random token in an HttpOnly + SameSite=Strict cookie, 8-hour TTL
        CSRFAll mutating requests require an x-csrf-token header (double-checked against the session)
        Brute-forcePer-IP rate limit: 5 failures / 60 seconds → 429
        TransportBinds localhost — for remote access, tunnel over SSH/VPN
        # reach the admin UI from another machine via SSH tunnel (safe, no open port)
        ssh -L 9464:127.0.0.1:9464 user@lb-server
        # then open http://127.0.0.1:9464 locally

        Endpoint

        MethodPathPurpose
        POST/api/loginLogin (publik)
        POST/api/logoutLogout
        GET/api/sessionCheck session + CSRF token
        GET/api/statsReal-time statistics
        GET/PUT/api/configRead / apply config
        POST/api/config/validateValidate with no side effects
        POST/api/log-modeToggle compact/full (saved to config file)
        GET/api/logsLog tail: ?after=<seq>&limit=<n> — last lines from the in-memory ring buffer
        GET/api/resourcesResource snapshot: server + process + active alerts + capacity advice
        GET/api/resources/historyChart history: ?window=fine|coarse&max=<n> (1h full-res / 7d per-minute)
        POST/api/restartDrain + re-exec the process
        GET/metricsPrometheus (public — localhost bind)
        GET/healthzHealth probe (publik)

        Linux tuning (production)

        # /etc/sysctl.d/99-lbrust.conf — lalu: sysctl --system
        
        # listen queue — match it with global.backlog
        net.core.somaxconn = 65535
        net.core.netdev_max_backlog = 65535
        
        # ephemeral ports for backend connections (the LB opens many)
        net.ipv4.ip_local_port_range = 1024 65535
        net.ipv4.tcp_tw_reuse = 1
        
        # buffer TCP
        net.core.rmem_max = 16777216
        net.core.wmem_max = 16777216
        net.ipv4.tcp_rmem = 4096 87380 16777216
        net.ipv4.tcp_wmem = 4096 65536 16777216
        
        # file descriptors (the LB holds thousands of sockets)
        fs.file-max = 2097152
        fs.nr_open = 2097152
        # per-process limit (systemd: add it to the unit file)
        ulimit -n 1048576
        # systemd unit:
        #   LimitNOFILE=1048576
        
        # verify after start
        cat /proc/$(pgrep -f 'lbrust run')/limits | grep 'open files'
        Benchmark note: a "10 million req/sec" claim is not realistic for a single process on 4–8 vCPUs (HAProxy on 64 cores reaches ≈ 2M HTTP RPS). lbrust strategy: single first → measure (wrk2/oha) → scale horizontally. Realistic 4–8 vCPU numbers: ~25k–60k HTTP RPS, higher for pure L4 TCP.

        FAQ & troubleshooting

        Admin login says "admin password is not set"

        admin.password_hash is still empty. Create the hash, then paste it:

        lbrust hash-password
        # → $argon2id$...  tempel ke [admin] password_hash di config
        Does host() match subdomains (wildcard)?

        No — host() is an exact match (faithful to HAProxy's hdr(host) -i); the port is ignored, so example.com:8443 still matches host('example.com'). For subdomains add explicit rules, e.g. host('api.example.com') or path_beg('/api'). Wildcards do apply to TLS certificates (*.example.com SAN), just not to routing rules.

        HTTPS works but the browser warns about the certificate

        Self-signed certificates are for testing only — issue a real one (certbot / Let's Encrypt) or add your internal CA to the trust store. The SSL/TLS section shows the config and checklist.

        How do I connect to a backend over TLS (e.g. CockroachDB)?

        Add ssl = { verify = false } to the server — every connection to it (traffic and health checks) is then wrapped in TLS. This matches HAProxy's server ... ssl verify none, which is the common setup for internal self-signed services: addr = "127.0.0.1:26250" + ssl = { verify = false }. For public endpoints use ssl = { verify = true, cafile = "/path/ca.pem" } to validate the certificate chain. See the [[backend]] section.

        Bind fails: Address already in use

        Another process owns the port. Check with: lsof -i :9000. For a zero-downtime move from old → new port, bind both temporarily (SO_REUSEPORT is already enabled in lbrust).

        Dashboard shows Log dropped climbing

        The log queue is full (slow disk or heavy traffic) — the data plane keeps running and the newest lines are dropped. Raise queue_capacity, avoid mode = "full", or move path to a faster disk.

        All servers are DOWN even though they are alive

        Check the log for backend DOWN: fall may have been reached (verify check.path for http checks), DNS hostname resolution may be failing, or passive backoff is active after repeated connect errors.

        Apply says "requires a process restart"

        The change touches bind/mode/log/admin — use the Restart button (drains first, then re-execs); SIGHUP is not enough here.

        Admin session is gone after a restart

        Expected — sessions live in process memory. Sign in again with the same password (the hash comes from the config file and does not change).

        How do I measure performance?
        # example: 200 connections, 30 seconds, target through lbrust
        wrk -t8 -c200 -d30s --latency http://127.0.0.1:9000/
        # or for pure L4:
        oha -c 200 -z 30s http://127.0.0.1:9000/

        Compare directly against HAProxy on the same machine & configuration.

        Restarting lbrust…

        contacting process…

        Sending restart command
        Draining active connections
        Waiting for the new process
        Reloading console