bext.dev
DocsOperations
Operations4 min read
On this page

Zero-Downtime Upgrades

bext-server supports graceful binary upgrades with zero connection drops. The running process hands its listening sockets to a new process, drains in-flight requests, and exits — all without a single failed request.

Quick Start#

bash
# 1. Install the new binary (atomic replace)
curl -fsSL https://bext.dev/install | sh

# 2. Trigger zero-downtime upgrade
bext upgrade

That's it. The old process finishes serving all in-flight requests while the new process immediately starts accepting new connections on the same port.

How It Works#

The upgrade uses socket FD inheritance — the same technique nginx uses for binary upgrades.

code
Operator                   Old process                New process
   │                           │                          │
   ├── bext upgrade ──────────►│                          │
   │   (sends SIGUSR2)         │                          │
   │                           ├── fork+exec ────────────►│
   │                           │   BEXT_UPGRADE_FDS=3,4   │
   │                           │   BEXT_UPGRADE_OLD_PID=X │
   │                           │                          ├── bind inherited FDs
   │                           │                          ├── start accepting
   │                           │◄── SIGQUIT ──────────────┤  (ready)
   │                           ├── stop accepting         │
   │                           ├── drain 30s              │
   │                           ├── exit(0)                │
   │                           ×                          │

Step by step#

  1. SIGUSR2 is sent to the running bext-server (via bext upgrade or kill -USR2)
  2. The running process forks and exec's the new binary at its own path
  3. Listening socket file descriptors are passed via the BEXT_UPGRADE_FDS environment variable
  4. The new process binds to the inherited FDs — no port conflict, no EADDRINUSE
  5. Once the new process is ready, it sends SIGQUIT to the old process
  6. The old process stops accepting new connections and drains in-flight requests (30s timeout)
  7. The old process exits cleanly

During steps 4-6, both processes are serving requests simultaneously — there is never a moment where no process is listening.

CLI Reference#

bext upgrade#

Sends SIGUSR2 to the running bext-server process.

bash
# Basic: re-exec current binary (e.g., after curl install)
bext upgrade

# Replace binary first, then upgrade
bext upgrade --binary /path/to/new-bext-server

# Specify PID file location
bext upgrade --pid-file /run/bext.pid

Flags:

Flag Default Description
--binary (none) Path to new binary. Atomically replaces the running binary before sending SIGUSR2.
--pid-file Auto-detect Path to PID file. Searches /run/bext.pid, /run/nginx.pid, /tmp/bext.pid.

Manual signal#

If you prefer manual control:

bash
# Find the PID
cat /run/bext.pid

# Send upgrade signal
kill -USR2 $(cat /run/bext.pid)

Systemd Integration#

bext's upgrade works seamlessly with systemd. The service file doesn't need any changes — bext upgrade handles everything:

ini
# /etc/systemd/system/bext.service
[Unit]
Description=bext application server
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/bext-server run
ExecReload=/usr/local/bin/bext-server upgrade
Restart=on-failure
PIDFile=/run/bext.pid

[Install]
WantedBy=multi-user.target

With this setup, systemctl reload bext triggers a zero-downtime upgrade.

Environment Variables#

These are set automatically during upgrade — you don't need to set them manually.

Variable Description
BEXT_UPGRADE_FDS Comma-separated list of inherited socket file descriptors
BEXT_UPGRADE_OLD_PID PID of the parent process to signal when ready

Signals Reference#

Signal Effect
SIGUSR2 Start zero-downtime upgrade (fork+exec new binary)
SIGQUIT Graceful shutdown with 30s drain (sent by new process)
SIGHUP Hot-reload V8 bundle and clear caches (no binary change)
SIGTERM Immediate shutdown

Drain Timeout#

The old process waits 30 seconds for in-flight requests to complete. Requests that take longer are terminated. This timeout covers:

  • Long-running SSR renders
  • WebSocket connections (gracefully closed)
  • SSE event streams (final event + close)
  • Pending PHP worker requests

Cache Behavior During Upgrade#

During upgrade, the new process starts with cold caches (ISR, tenant, compression). This means:

  • First few requests after upgrade may be slower (cache miss → render → cache fill)
  • The ISR stale-while-revalidate mechanism ensures users still see cached content while caches warm
  • Tenant cache is populated on first request per hostname
  • Compression cache fills within seconds under load

If you're running with Redis L2 cache enabled, cached content survives the upgrade since Redis is external.

Automating Upgrades#

Cron-based auto-update#

bash
# /etc/cron.d/bext-upgrade
0 3 * * * root curl -fsSL https://bext.dev/install | sh && bext upgrade

CI/CD pipeline#

yaml
# .github/workflows/upgrade.yml
- name: Upgrade bext
  run: |
    ssh deploy@server 'curl -fsSL https://bext.dev/install | sh && bext upgrade'

Webhook-triggered#

Use bext's git deploy webhook to trigger upgrades on push:

toml
# bext.config.toml
[git_deploy]
enabled = true
secret = "your-webhook-secret"
on_push = "curl -fsSL https://bext.dev/install | sh && bext upgrade"

Troubleshooting#

Upgrade failed: "cannot resolve own binary"#

The process can't find its own executable path. This happens in some containerized environments. Set the binary path explicitly:

bash
bext upgrade --binary /usr/local/bin/bext-server

Upgrade failed: "failed to spawn new process"#

Check file permissions on the binary:

bash
ls -la $(which bext-server)
# Should be: -rwxr-xr-x

Old process didn't exit after 30s#

The drain timeout expired with requests still in flight. Check for:

  • Very long-running requests (increase timeout if needed)
  • WebSocket connections that didn't close
  • Stuck PHP worker processes

Port conflict on startup#

If you see EADDRINUSE during upgrade, the FD inheritance failed. This can happen if:

  • The binary was compiled without the upgrade module
  • The BEXT_UPGRADE_FDS env var was lost (check your process supervisor)
Tip

If you're running with Redis L2 cache enabled, the populated cache survives the binary upgrade — only the in-process L1 and compression caches start cold. Under moderate traffic the caches warm within seconds.

  • Scaling — graceful shutdown, drain timeout, and Redis L2 cache behavior
  • Invalidation & Restarts — per-site and per-cache surfaces for the smallest blast radius
  • Deploying: Generatorsbext systemd gen with --socket-activation for the systemd upgrade path
  • Monitoring — health check endpoint used by load balancers during rolling restarts
  • Troubleshooting — common post-upgrade issues
Edit this page ↗Need a hand? ↗
FIND YOUR NEXT STEP

Start with a topic, a command, or a question.