# Configuration: `scalws.yaml`

URL: https://scalws.com/docs/guide/configuration/
Source: `docs/guide/configuration.md` in the ScalWS repository

ScalWS reads one YAML file, by default `/etc/scalws/scalws.yaml`. This page is an overview
of that file: its layout, what each part of the `server:` section is for, the caveats worth
knowing, and an example per section. Every key, with its syntax, default, context and
reload behaviour, is listed in the [configuration reference](https://scalws.com/docs/reference/).
Tenants, applications and runtimes have their own guide: [applications.md](https://scalws.com/docs/guide/applications/).

Validate a file before you apply it:

```sh
scalwsctl check                          # validate + prepare exactly like a reload
scalwsctl check /path/to/scalws.yaml
```

Every section rejects unknown keys, so a typo such as `idle_timout` is an error, not a
silently ignored setting. Semantic problems are collected and reported together, each with
its location (for example `server.limits.max_headers: must be between 1 and 1024`), so you
can fix a file in one pass. Some checks produce warnings instead of errors; they are logged
at start-up.

The file may be at most 16 MiB.

## Contents

* [File layout](#file-layout)
* [Value formats](#value-formats)
* [`server` sections](#server)
  * [listeners](#listeners) · [limits](#limits) · [metrics](#metrics) · [logging](#logging) ·
    [runtime_dir](#runtime_dir) · [cgroups](#cgroups) · [cache](#cache) ·
    [security](#security) · [optimizer](#optimizer) · [diagnostics](#diagnostics) ·
    [ai](#ai) · [admin](#admin) · [acme](#acme) · [trusted_proxies](#trusted_proxies) ·
    [sendfile](#sendfile) · [ktls](#ktls) · [open_file_cache](#open_file_cache) ·
    [event_loops](#event_loops) · [ebpf](#ebpf) · [listener_mode](#listener_mode) ·
    [listen_socket](#listen_socket) · [loop_lag_probe](#loop_lag_probe) ·
    [accept_policy](#accept_policy) · [upstream_client](#upstream_client) ·
    [http1_server](#http1_server) · [sandbox](#sandbox)
* [`upstreams`](#upstreams)
* [Tenants and applications](#tenants-and-applications)
  * [proxy upstream and backends](#proxy-upstream-and-backends) ·
    [route matching](#route-matching) · [route policies](#route-policies) ·
    [redirect fields](#redirect-fields)
* [Complete production example](#complete-production-example)
* [Reload or restart?](#reload-or-restart)

## File layout

The file has four top-level keys:

* [`include`](https://scalws.com/docs/reference/top-level/#include): site files read in addition to this
  file ([ADR-0048](https://scalws.com/docs/adr/0048-site-files/), [sites.md](https://scalws.com/docs/sites/)).
* [`server`](https://scalws.com/docs/reference/top-level/#server) (**required**): server-wide settings:
  listeners, limits, observability, caches, security and so on.
* [`upstreams`](#upstreams) (default empty): named pools of upstream members for `proxy`
  runtimes ([load-balancing.md](https://scalws.com/docs/guide/load-balancing/)). Only the main file may define them.
* [`tenants`](https://scalws.com/docs/reference/tenants/#tenants) (default `[]`): tenants and their
  applications. See [applications.md](https://scalws.com/docs/guide/applications/).

Every top-level key: [reference](https://scalws.com/docs/reference/top-level/).

```yaml
include: sites.d/*.yaml

server:
  listeners:
    - name: http
      address: "0.0.0.0:80"

tenants:
  - name: acme
    applications: [...]
```

### `include`

[`include`](https://scalws.com/docs/reference/top-level/#include) names site files: one short file per site
(or a file with its own `tenants:` list). The loader reads them, appends their tenants and
applications to `tenants`, and then validates the whole result as one configuration.
`scalwsctl site add|list|remove` manages these files; see [sites.md](https://scalws.com/docs/sites/) for their
format.

* A single string (`include: sites.d/*.yaml`) or a list (`include: [sites.d/*.yaml, extra.yaml]`).
* Relative paths are resolved against the directory of the main file.
* Each entry is either one file, or a directory plus a file pattern with exactly one `*`
  in the file name (`sites.d/*.yaml`). A `*` in the directory part, more than one `*`,
  or naming a directory itself (`sites.d`) is an error.
* Hidden files (names starting with `.`) are skipped; matches are read in sorted order,
  and a file matched by several entries is read once.
* At most 10,000 site files; each at most 16 MiB.
* On Unix, each site file and its directory must belong to root (or to the owner of the
  main file) and must not be writable by group or others (`chmod go-w`): whoever can
  write a site file can serve any domain from any directory the server can read.
* Only the main file may use `include`; site files cannot include further files.

The packaged default configuration already contains `include: sites.d/*.yaml`.

## Value formats

Several keys take values with units; the full rules are in
[value formats](https://scalws.com/docs/reference/value-formats/). In short:

* **Sizes**: an integer (bytes) or a string with a unit: binary `KiB`, `MiB`, `GiB`,
  `TiB` (powers of 1024) or decimal `KB` (or `kB`), `MB`, `GB`, `TB` (powers of 1000).
  Examples: `16KiB`, `10MiB`, `10MB`, `1048576`. Units are case-sensitive and the number
  must be whole.
* **Durations**: a string, a whole number followed by one unit: `ms`, `s`, `m`, `h` or
  `d` (`500ms`, `30s`, `5m`, `12h`, `30d`). Bare numbers and combinations such as `1m30s`
  are rejected; write `90s`.
* **CPU quotas** (tenant resources): a percentage of one CPU as a quoted string;
  `"100%"` is one CPU, `"400%"` four.
* **Addresses**: `IP:port` with an IP literal (no host names): `"0.0.0.0:443"`,
  `"127.0.0.1:9901"`, `"[::]:443"`. Quote them in YAML. In `server.listeners`, a listener
  on the IPv6 wildcard `[::]:p` takes IPv6 only (`IPV6_V6ONLY`) when another listener there
  uses an IPv4 address (`0.0.0.0` or a specific one) on the same port `p`, which then takes
  IPv4. Without such a listener `[::]:p` keeps the system's default: dual-stack on Linux
  (`net.ipv6.bindv6only=0`), serving IPv4 too. HTTP/3's UDP sockets follow the same rule.
  Under scalws-controller a reload that changes the pairing of `[::]:p` gets a new TCP
  socket of the right mode (instances of the old generation keep the old one until they
  drain); for a UDP socket in use (`http3: true` on `[::]:p`) the reload is refused with the
  reason, because a UDP address cannot be bound twice: stop the controller, its instances and
  the QUIC demux to change it.
* **Names**: listener, tenant and application names are 1 to 63 characters of `a-z`,
  `0-9`, `-` and `_`, starting with a lower-case letter. They appear in metrics and logs.

## `server`

Only [`listeners`](https://scalws.com/docs/reference/server/#server.listeners) is required; every other
section is optional and has production defaults. The sections below follow the order of
the configuration model. All keys directly under `server`:
[reference](https://scalws.com/docs/reference/server/).

### `listeners`

**Required**, a list with at least one entry. Each listener accepts HTTP/1.1 and HTTP/2
(and, optionally, HTTP/3) on one [`address`](https://scalws.com/docs/reference/server-listeners/#server.listeners-.address).
A listener without [`tls`](https://scalws.com/docs/reference/server-listeners/#server.listeners-.tls) serves
plain HTTP. [`strict_sni`](https://scalws.com/docs/reference/server-listeners/#server.listeners-.strict_sni)
prevents domain fronting across tenants ([ADR-0013](https://scalws.com/docs/adr/0013-deterministic-security/));
[`http3`](https://scalws.com/docs/reference/server-listeners/#server.listeners-.http3) adds HTTP/3 over UDP on
the same port, off by default ([tls.md](https://scalws.com/docs/guide/tls/#http3-quic), [ADR-0019](https://scalws.com/docs/adr/0019-http3/));
[`proxy_protocol`](https://scalws.com/docs/reference/server-listeners/#server.listeners-.proxy_protocol)
reads the client address from a load balancer ([ADR-0022](https://scalws.com/docs/adr/0022-trusted-proxies/)).

Constraints:

* Listener names and addresses must be unique. Port `0` (an ephemeral port chosen by the
  OS) is exempt from the duplicate check.
* A listener may not use the same address as the metrics endpoint or the remote admin
  endpoint.
* `strict_sni: true` is only allowed on TLS listeners.
* `http3: true` needs `tls` and is not allowed with `listener_mode: reuseport`. The
  packages include HTTP/3; a scalwsd built from source without the `http3` feature refuses
  the setting at start.
* `proxy_protocol: true` needs [`trusted_proxies`](#trusted_proxies): connections from
  peers outside that list are closed. The header is read within
  `limits.header_read_timeout`; a missing or malformed header closes the connection.

#### `tls`

A TLS listener needs at least one entry in
[`certificates`](https://scalws.com/docs/reference/server-listeners/#server.listeners-.tls.certificates), or
[`acme: true`](https://scalws.com/docs/reference/server-listeners/#server.listeners-.tls.acme) to also serve
the ACME-managed certificates of applications with `acme: true` (see [`acme`](#acme)).

Within one listener, a server name may be assigned to only one certificate, and at most
one certificate may be the [`default`](https://scalws.com/docs/reference/server-listeners/#server.listeners-.tls.certificates-.default).
The default entry may have `server_names: []`; it is then served only to clients that send
no SNI name or one no other entry lists. Every other entry needs at least one name.
Names are compared case-insensitively, without a trailing dot; a wildcard covers exactly
one label and must have a multi-label suffix (`*.example.com`, not `*.com`). Use punycode
for internationalised names. Certificate files are read when the configuration is
prepared (start, reload, `scalwsctl check`), not during validation.

More on certificates: [tls.md](https://scalws.com/docs/guide/tls/). Every key of `server.listeners`, including `tls`
and its certificates, with its syntax, default and reload behaviour:
[reference](https://scalws.com/docs/reference/server-listeners/).

```yaml
server:
  listeners:
    - name: http
      address: "0.0.0.0:80"
    - name: https
      address: "0.0.0.0:443"
      strict_sni: true
      tls:
        acme: true
        certificates:
          - cert: /etc/scalws/tls/example.com.pem   # leaf first, then intermediates
            key: /etc/scalws/tls/example.com.key
            server_names: [example.com, "*.example.com"]
            default: true
          - cert: /etc/scalws/tls/shop.example.org.pem
            key: /etc/scalws/tls/shop.example.org.key
            server_names: [shop.example.org]
```

### `limits`

Connection and request limits. Every key is optional; the defaults are conservative
production values. The ones most often changed are
[`max_request_body_bytes`](https://scalws.com/docs/reference/server-limits/#server.limits.max_request_body_bytes)
(uploads) and the timeouts.

Caveats and constraints not shown in the reference:

* [`max_connections`](https://scalws.com/docs/reference/server-limits/#server.limits.max_connections) counts
  HTTP/3 connections too and must be at least 1.
* `max_header_bytes` must be between `8KiB` and `1MiB`; `max_headers` between 1 and 1024;
  `max_uri_bytes` at least 256 bytes and at most `max_header_bytes`.
* `idle_timeout` is also the QUIC idle timeout for HTTP/3, and `h2_max_concurrent_streams`
  (1 to 10000) also limits bidirectional streams per HTTP/3 connection.
* `tls_handshake_timeout`, `header_read_timeout`, `body_read_timeout`, `idle_timeout`,
  `upgraded_idle_timeout` and `write_timeout` must be above zero.
* `shutdown_timeout` is how long connections may drain on shutdown before they are closed.
* `shutdown_delay` (default `0s`) keeps serving for that long after SIGTERM while
  `/readyz` answers 503, so a load balancer or Kubernetes stops sending requests first;
  the drain (`shutdown_timeout`) starts after it. A second SIGTERM ends the delay. Both
  are read when the signal arrives, so a reload changes them.

Every key of `server.limits`, with its syntax, default and reload behaviour:
[reference](https://scalws.com/docs/reference/server-limits/).

```yaml
server:
  limits:
    max_request_body_bytes: 64MiB    # e.g. for uploads
    idle_timeout: 30s
```

Per-client limits are in [`security.per_ip`](#security); per-tenant quotas are tenant
`resources` ([applications.md](https://scalws.com/docs/guide/applications/)).

### `metrics`

A loopback HTTP endpoint for monitoring ([ADR-0005](https://scalws.com/docs/adr/0005-observability/)). It
serves `/metrics` (Prometheus text format) and `/healthz`, plus read-only views used by
`scalwsctl` and a read-only dashboard at `/dashboard` (see [../OPERATIONS.md](https://scalws.com/docs/operations/)).
[`hosts`](https://scalws.com/docs/reference/server-metrics/#server.metrics.hosts) adds per-domain counters
([ADR-0042](https://scalws.com/docs/adr/0042-per-domain-counters/)).

The endpoint has no authentication. Its
[`address`](https://scalws.com/docs/reference/server-metrics/#server.metrics.address) must differ from every
listener address; a non-loopback address is accepted but produces a warning; keep it on
loopback or behind a firewall.

The packaged default configuration sets `address: "127.0.0.1:9900"`; the built-in
default, when the key is omitted, is `127.0.0.1:9901`.

Every key of `server.metrics`: [reference](https://scalws.com/docs/reference/server-metrics/).

```yaml
server:
  metrics:
    address: "127.0.0.1:9900"
    max_hosts: 5000
```

### `logging`

Log format, level and access logging. The `SCALWS_LOG` environment variable, if set,
takes precedence over [`level`](https://scalws.com/docs/reference/server-logging/#server.logging.level).
`format` and `level` are applied when scalwsd starts.

Every key of `server.logging`: [reference](https://scalws.com/docs/reference/server-logging/).

```yaml
server:
  logging:
    format: text
    level: "info,scalws_proxy=debug"
    access_log: false
```

### `runtime_dir`

[`runtime_dir`](https://scalws.com/docs/reference/server/#server.runtime_dir) (default `/run/scalws`) is the
directory for managed runtimes' sockets and generated configuration
([ADR-0008](https://scalws.com/docs/adr/0008-process-supervision/)); also the default location of the admin
socket. It must be writable by scalws.

Constraints: an absolute path, at most 64 bytes (Unix socket paths below it must fit the
kernel limit), using only `A-Z a-z 0-9 / _ . -`.

### `cgroups`

cgroup v2 placement of managed applications, which enforces tenant `resources` such as
`cpu`, `memory`, `pids` and I/O limits ([ADR-0010](https://scalws.com/docs/adr/0010-cgroups-and-tenant-limits/)).
[`mode`](https://scalws.com/docs/reference/server-cgroups/#server.cgroups.mode) `auto` (the default) enforces
limits when cgroup delegation works and otherwise warns and runs without enforcement;
`required` refuses to start without working delegation; `off` uses no cgroups.

Constraint: [`path`](https://scalws.com/docs/reference/server-cgroups/#server.cgroups.path) must start with
`/sys/fs/cgroup/`.

With `mode: off`, tenant process limits are not enforced (a warning lists the tenants
that set them), and [`ebpf.network`](#ebpf) cannot be used.

Every key of `server.cgroups`: [reference](https://scalws.com/docs/reference/server-cgroups/).

```yaml
server:
  cgroups: {mode: required}
```

### `cache`

Server-wide budget of the response cache ([ADR-0012](https://scalws.com/docs/adr/0012-response-cache/)).
Nothing is cached unless an application enables `cache:` (see [caching.md](https://scalws.com/docs/guide/caching/)).
[`max_entry_size`](https://scalws.com/docs/reference/server-cache/#server.cache.max_entry_size) must be above
zero and at most [`memory`](https://scalws.com/docs/reference/server-cache/#server.cache.memory).

#### `cache.disk`

The [disk tier](https://scalws.com/docs/reference/server-cache/#server.cache.disk)
([ADR-0024](https://scalws.com/docs/adr/0024-cache-disk-tier/)) receives entries evicted from memory and
entries larger than `max_entry_size`, and keeps them across restarts. When it is
configured but unusable, the server does not start. The directory holds response headers
and bodies: keep it private to the service user.

`path` and `size` are required; `path` must be absolute, `size` above zero, and the disk
tier's own `max_entry_size` above zero and at most `size`.

Every key of `server.cache`, including `disk`: [reference](https://scalws.com/docs/reference/server-cache/).

```yaml
server:
  cache:
    memory: 256MiB
    max_entry_size: 8MiB
    disk: {path: /var/lib/scalws/cache, size: 10GiB}
```

### `security`

Server-wide security settings ([ADR-0013](https://scalws.com/docs/adr/0013-deterministic-security/)). Path
rules and per-route rate limits belong to applications; see [security.md](https://scalws.com/docs/guide/security/).
Without [`per_ip`](https://scalws.com/docs/reference/server-security/#server.security.per_ip) there are no
per-client limits.

#### `security.per_ip`

Clients are identified by IPv4 address or by IPv6 `/64` network. Behind a load balancer,
set [`trusted_proxies`](#trusted_proxies) so the real client address is used.

* Excess connections over
  [`max_connections`](https://scalws.com/docs/reference/server-security/#server.security.per_ip.max_connections)
  are closed at accept; requests over `requests_per_second` (a token bucket) get `429`.
* `max_connections`, `requests_per_second` and `burst` must be above zero; `burst` needs
  `requests_per_second`; `table_size` must be at least 64.

When the table is full, the least recently seen idle entry is evicted; if every entry is
in use, a new client is admitted untracked (and counted) while the global
`limits.max_connections` still applies. Connections from trusted proxies are exempt from
the per-client connection limit; their requests are limited per client.

Every key of `server.security`: [reference](https://scalws.com/docs/reference/server-security/).

```yaml
server:
  security:
    per_ip: {max_connections: 256, requests_per_second: 50, burst: 200}
```

### `optimizer`

Deterministic optimizer ([ADR-0014](https://scalws.com/docs/adr/0014-deterministic-optimizer/)): evaluates
every application each [`interval`](https://scalws.com/docs/reference/server-optimizer/#server.optimizer.interval)
and records decisions (for example worker-count recommendations). View them with
`scalwsctl recommendations`. Diagnostics are evaluated on the same interval.

Constraints: `interval` between `1s` and `1h`; `journal_size` between 1 and 100000.

Whether the optimizer may act (rather than only recommend) is set per worker pool with
`workers.autoscale`; see [applications.md](https://scalws.com/docs/guide/applications/).

Every key of `server.optimizer`: [reference](https://scalws.com/docs/reference/server-optimizer/).

### `diagnostics`

Diagnostics ([ADR-0015](https://scalws.com/docs/adr/0015-diagnostics/)); findings are evaluated every
`optimizer.interval`. View them with `scalwsctl diagnose [tenant/app]`.
[`slow_request`](https://scalws.com/docs/reference/server-diagnostics/#server.diagnostics.slow_request) must
be above zero and `recent_requests` at most 10000.

Every key of `server.diagnostics`: [reference](https://scalws.com/docs/reference/server-diagnostics/).

```yaml
server:
  diagnostics: {slow_request: 2s, recent_requests: 50}
```

### `ai`

Local LLM advisor ([ADR-0016](https://scalws.com/docs/adr/0016-local-ai-advisor/)). **Disabled by default and
advisory only**: it explains diagnoses (`scalwsctl explain tenant/app`); it never changes
configuration, never runs commands, and the server and optimizer work the same when it is
down. Input is limited to aggregated, redacted telemetry.

The [`endpoint`](https://scalws.com/docs/reference/server-ai/#server.ai.endpoint) is any OpenAI-compatible
chat-completions URL (llama.cpp server, Ollama, vLLM, ...). When
[`enabled`](https://scalws.com/docs/reference/server-ai/#server.ai.enabled) is `true`, these checks apply
(when it is `false`, the other keys are not checked):

* `endpoint` is an `http://` or `https://` URL with a host and no user info, on loopback
  unless `allow_remote` is set (telemetry then leaves the host);
* `model` is required;
* `api_key_env` names an environment variable (letters, digits, `_`); the key itself
  never goes in the file;
* `timeout` is between `1s` and `10m`, `max_queue` between 1 and 64, `max_tokens` between
  64 and 8192.

Every key of `server.ai`: [reference](https://scalws.com/docs/reference/server-ai/).

```yaml
server:
  ai:
    enabled: true
    endpoint: "http://127.0.0.1:11434/v1/chat/completions"
    model: "qwen2.5:7b-instruct"
```

### `admin`

Admin API for `scalwsctl` on a Unix socket ([ADR-0017](https://scalws.com/docs/adr/0017-admin-api-and-operations/)),
and optional remote administration over mutual TLS ([ADR-0023](https://scalws.com/docs/adr/0023-remote-admin-mtls/)).
Usage: [cli.md](https://scalws.com/docs/guide/cli/), [../OPERATIONS.md](https://scalws.com/docs/operations/).

* [`socket`](https://scalws.com/docs/reference/server-admin/#server.admin.socket) must be an absolute path
  of at most 100 bytes.
* [`remote`](https://scalws.com/docs/reference/server-admin/#server.admin.remote) is off unless set; when
  set, all four keys (`address`, `cert`, `key`, `client_ca`) are required, the paths must
  be absolute, and `address` must not be a listener or metrics address. Only clients
  with a certificate issued by `client_ca` can connect.

Every key of `server.admin`: [reference](https://scalws.com/docs/reference/server-admin/).

```yaml
server:
  admin:
    socket: /run/scalws/admin.sock
    remote:
      address: "0.0.0.0:9443"
      cert: /etc/scalws/admin.pem
      key: /etc/scalws/admin.key
      client_ca: /etc/scalws/operators-ca.pem
```

### `acme`

Automatic certificates ([ADR-0018](https://scalws.com/docs/adr/0018-acme/)) for applications with
`acme: true`, using HTTP-01 challenges. The default
[`directory`](https://scalws.com/docs/reference/server-acme/#server.acme.directory) is Let's Encrypt
production. See also [tls.md](https://scalws.com/docs/guide/tls/).

When [`enabled`](https://scalws.com/docs/reference/server-acme/#server.acme.enabled) is `true`:

* `terms_of_service_agreed` must be `true`;
* `directory` must be an `https://` URL;
* each `contact` entry must be a `mailto:` address;
* `check_interval` must be at least `1m`;
* at least one TLS listener must have `tls: {acme: true}`;
* at least one plain HTTP listener must exist (port 80 in production) for HTTP-01.

Applications with `acme: true` require `enabled: true`. Their domains must be public DNS
names: wildcards (`*.example.com`, which would need DNS-01), IP addresses and `localhost`
are rejected. Use a static certificate for those.

Every key of `server.acme`: [reference](https://scalws.com/docs/reference/server-acme/).

Try a new setup against the Let's Encrypt staging directory first:

```yaml
server:
  listeners:
    - {name: http, address: "0.0.0.0:80"}                       # HTTP-01 challenges
    - {name: https, address: "0.0.0.0:443", tls: {acme: true}}
  acme:
    enabled: true
    directory: https://acme-staging-v02.api.letsencrypt.org/directory
    contact: ["mailto:ops@example.com"]
    terms_of_service_agreed: true
```

`scalwsctl certs` shows each application's names, days left and errors. Renewed
certificates are activated without a restart.

### `trusted_proxies`

[`trusted_proxies`](https://scalws.com/docs/reference/server/#server.trusted_proxies) (default `[]`) lists
the peers allowed to report the client address via `X-Forwarded-For` or the PROXY protocol
([ADR-0022](https://scalws.com/docs/adr/0022-trusted-proxies/)), typically your load balancers. Entries are
CIDRs (`192.0.2.0/24`, `2001:db8::/32`) or single addresses (`192.0.2.10`, treated as `/32`
or `/128`).

* For requests from a trusted peer, the client is the right-most `X-Forwarded-For` entry
  that is not itself a trusted proxy. From untrusted peers the header is ignored and
  replaced.
* Listeners with `proxy_protocol: true` require this list and close connections from
  other peers.
* The effective client address is used for per-client limits, access logs, PHP
  `REMOTE_ADDR` and diagnostics.

```yaml
server:
  trusted_proxies: ["192.0.2.0/24", "192.0.2.10"]
```

### `sendfile`

[`sendfile`](https://scalws.com/docs/reference/server/#server.sendfile) (default `true`) sends large static
files on plain-TCP HTTP/1 connections with `sendfile(2)` (Linux,
[ADR-0027](https://scalws.com/docs/adr/0027-sendfile/)). For HTTPS see [`ktls`](#ktls).

### `ktls`

[`ktls`](https://scalws.com/docs/reference/server/#server.ktls) (default `false`, opt-in) enables kernel TLS
for HTTPS/1.1 file bodies (Linux, [ADR-0030](https://scalws.com/docs/adr/0030-ktls/)), so large files can use
`sendfile` over TLS too. Load the kernel module first (`modprobe tls`, and persist it in
`/etc/modules-load.d/tls.conf`). scalwsd probes kernel support at start; if the module is
missing it logs a warning and keeps TLS in user space. HTTP/2 connections stay on
user-space TLS.

### `open_file_cache`

Open file cache for static files ([ADR-0028](https://scalws.com/docs/adr/0028-open-file-cache/)), like nginx's
`open_file_cache` / `open_file_cache_valid`.
[`max_entries`](https://scalws.com/docs/reference/server-open_file_cache/#server.open_file_cache.max_entries)
is per document root (`0` disables the cache). With
[`valid`](https://scalws.com/docs/reference/server-open_file_cache/#server.open_file_cache.valid) unset (or
`0s`), every hit is checked against the file, so changed files are served at once; a
longer value trades that for fewer checks.

Every key of `server.open_file_cache`: [reference](https://scalws.com/docs/reference/server-open_file_cache/).

```yaml
server:
  open_file_cache: {max_entries: 20000, valid: 60s}   # changes visible within 60s
```

### `event_loops`

[`event_loops`](https://scalws.com/docs/reference/server/#server.event_loops) (an integer from 0 to 65535,
unset by default) sets the per-core event loops ([ADR-0029](https://scalws.com/docs/adr/0029-per-core-event-loops/)):

* unset: one event loop per CPU scalwsd may run on (honours `taskset` and cgroup CPU
  limits);
* `N`: N event loops;
* `0`: one shared work-stealing runtime.

Per-core loops are Linux only; elsewhere scalwsd always uses the shared runtime. When the
file leaves it unset, `scalwsd --worker-threads N` sets the count.

### `ebpf`

Optional eBPF enrichment ([ADR-0025](https://scalws.com/docs/adr/0025-ebpf-network-accounting/)). **Opt-in.**
[`network`](https://scalws.com/docs/reference/server-ebpf/#server.ebpf.network) exports per-application
network bytes and packets (`scalws_app_network_bytes`, `scalws_app_network_packets`).

Requirements: a build with the `ebpf` feature (a build without it refuses the setting at
start), cgroup enforcement (`cgroups.mode` must not be `off`), and `CAP_BPF` +
`CAP_NET_ADMIN` (or root). The packaged systemd unit does not grant `CAP_BPF`. If the
programs cannot be loaded, scalwsd logs a warning and runs without them. Traffic over Unix
sockets (scalws to its applications, by default) is not counted.

Every key of `server.ebpf`: [reference](https://scalws.com/docs/reference/server-ebpf/).

### `listener_mode`

[`listener_mode`](https://scalws.com/docs/reference/server/#server.listener_mode) (`shared`, `reuseport` or
`inherited`; default `shared`) sets how listening sockets are opened
([ADR-0033](https://scalws.com/docs/adr/0033-adaptive-runtime-control-and-data-plane/)):

* `shared` (default): one socket per address, shared by this process's event loops.
* `reuseport`: one `SO_REUSEPORT` socket per event loop, so several scalwsd instances can
  serve the same address. Linux only; needs per-core event loops (`event_loops` must not
  be `0`); HTTP/3 listeners are not allowed in this mode.
* `inherited`: the sockets are received from [`listen_socket`](#listen_socket) (opened by
  scalws-controller; every instance accepts from the same queue). Linux only; needs
  `listen_socket`.

You normally do not set this by hand: scalws-controller starts its instances with the
right mode (`--reuseport` or `--listen-socket`). See [../adaptive-runtime.md](https://scalws.com/docs/adaptive-runtime/).

### `listen_socket`

[`listen_socket`](https://scalws.com/docs/reference/server/#server.listen_socket) is the Unix socket that
hands out the listening sockets for `listener_mode: inherited`. Set by scalws-controller
through `--listen-socket`; required when `listener_mode` is `inherited`.

### `loop_lag_probe`

[`loop_lag_probe`](https://scalws.com/docs/reference/server/#server.loop_lag_probe) (default `false`)
measures event-loop lag (a 100 ms timer per loop) and exports
`scalws_event_loop_lag_seconds`, the Adaptive Runtime's pressure signal
([ADR-0034](https://scalws.com/docs/adr/0034-pressure-score-and-bottleneck-classification/)). Off by default;
turned on for instances started by the controller.

### `accept_policy`

[`accept_policy`](https://scalws.com/docs/reference/server/#server.accept_policy) (`shared` or `fountain`;
default `shared`) chooses which event loop takes a new connection from the shared accept
queue ([ADR-0044](https://scalws.com/docs/adr/0044-fountain-accept-policy/)):

* `shared` (default): whichever loop wakes first.
* `fountain` (**opt-in**): loops fill in order; a loop takes connections only while the
  loops before it are busy or hold twice their fair share of the open connections (at
  least 8 each). Established connections never move. It saves CPU with many short
  connections; connections opened in one burst can still end up on a few loops, so keep
  `shared` where keep-alive latency matters (a proxy, an ingress controller). It has
  no effect with a socket per event loop (`listener_mode: reuseport` or
  `SCALWS_REUSEPORT=1`): the kernel then picks the loop, and scalwsd logs this once at
  startup.

### `upstream_client`

[`upstream_client`](https://scalws.com/docs/reference/server/#server.upstream_client) (`hyper` or
`experimental`; default `hyper`) is the HTTP/1 client used for every proxy and managed
application (Node.js, Python) ([ADR-0045](https://scalws.com/docs/adr/0045-experimental-upstream-client/)).

* `hyper` (default).
* `experimental`: scalws's own client. **Experimental, opt-in**, not the default; it is
  meant for measurement until it has proven itself on real applications.

A proxy route's `client:` overrides this per route (see [applications.md](https://scalws.com/docs/guide/applications/)).
This server-wide value is read at start-up: a reload keeps the running value, while
route-level `client` changes apply on reload.

### `http1_server`

[`http1_server`](https://scalws.com/docs/reference/server/#server.http1_server) (`hyper` or `experimental`;
default `hyper`) selects the HTTP/1 server implementation
([ADR-0046](https://scalws.com/docs/adr/0046-own-http1-server/)).

* `hyper` (default).
* `experimental`: scalws's own HTTP/1.1 server side, with stricter request framing.
  **Experimental, opt-in**, not the default. HTTP/2, HTTP/3 and TLS connections that
  negotiate another protocol keep their current implementation.

A change applies to connections accepted after a reload.

### `sandbox`

[`sandbox`](https://scalws.com/docs/reference/server/#server.sandbox) (`off`, `audit` or `on`; default `off`)
confines `scalwsd` with Landlock and seccomp. The first install of the package adds
`sandbox: on`. When it applies, what it allows and how to use `audit`:
[sandbox.md](https://scalws.com/docs/guide/sandbox/).

## `upstreams`

`upstreams` (default empty) is a map of named pools of upstream members. A `proxy`
runtime sends to a pool with `upstream: {pool: name}` or splits requests across pools with
`backends` (see [proxy upstream and backends](#proxy-upstream-and-backends)). Balancing,
affinity, passive health, retries and run-time member changes are explained in
[load-balancing.md](https://scalws.com/docs/guide/load-balancing/); the design is
[ADR-0049](https://scalws.com/docs/adr/0049-upstream-pools-and-runtime-members/).

Pool names follow the name rule (1 to 63 characters of `a-z`, `0-9`, `-` and `_`, starting
with a lower-case letter). Pools live in the main file only; site files can use them.

| Field | Type | Default | Description |
|---|---|---|---|
| `members` | list | `[]` | Members of the pool, each `{address, weight}`. Optional with `members_from: api`, where it only seeds a new pool. A file pool without members loads with a warning; its requests get `503`. |
| `members[].address` | string | required | `ip:port`, `[ipv6]:port`, `host:port` (resolved when a connection is made) or `unix:/absolute/path` (only as the single member of a plain-HTTP pool). The port is required; no scheme, no path, no duplicates; at most 10,000 members. |
| `members[].weight` | integer | `1` | Relative share of requests, 1 to 1000. |
| `members_from` | `file` or `api` | `file` | Where the member list comes from: this file, or the admin API (`PUT /v1/upstreams/{name}`, [admin API](https://scalws.com/docs/reference/admin-api/#put-v1upstreamsname)). |
| `balance` | `round_robin`, `least_conn` or `hash` | `round_robin` | Smooth weighted round robin; fewest requests in flight per weight; or weighted rendezvous hashing of `hash.by`. |
| `hash.by` | `cookie`, `header` or `client_ip` | required with `balance: hash` | Key of the hash: an affinity cookie, a request header, or the client address after `trusted_proxies`. |
| `hash.name` | string | `SCALWS_AFFINITY` for cookies | Cookie or header name (token characters, at most 64); required with `by: header`, not allowed with `by: client_ip`. |
| `hash.cookie_max_age` | duration | `0s` | `Max-Age` of the affinity cookie; `0s` makes a session cookie. Only with `by: cookie`. |
| `passive_health.failures` | integer | `3` | Consecutive failures before a member is ejected, 0 to 100; `0` turns passive health off. |
| `passive_health.cooldown` | duration | `10s` | How long an ejected member stays out, `1ms` to `1h`. |
| `retries` | integer | `1` | Further attempts on other members, 0 to 5 (only for pools with more than one member; see [retries](https://scalws.com/docs/guide/load-balancing/#retries)). |
| `keepalive` | integer | `32` | Idle connections kept per member, at most 1024. |
| `scheme` | `http` or `https` | `http` | Protocol towards the members. With `https`, certificates are always verified. |
| `tls.ca_file` | path | none | Absolute path of a PEM bundle trusted in addition to the system roots. Only with `scheme: https`. |
| `tls.server_name` | string | the member's host | SNI and certificate name (a DNS name without wildcard, or an IP address). Only with `scheme: https`. |

```yaml
upstreams:
  api:
    members:
      - {address: "192.0.2.5:8080", weight: 2}
      - {address: "192.0.2.6:8080"}
    balance: least_conn
    passive_health: {failures: 5, cooldown: 30s}
  sessions:
    members_from: api                # members are set with scalwsctl upstreams set
    balance: hash
    hash: {by: cookie, cookie_max_age: 1d}
```

Every change under `upstreams` applies on reload. A pool whose settings (everything except
`members`) are unchanged is kept with its connections and health; a pool whose settings
changed is rebuilt. Members of an `api` pool are kept across reloads, but not across a
restart.

## Tenants and applications

[`tenants`](https://scalws.com/docs/reference/tenants/#tenants) is a list of tenants; each tenant has a
`name`, an optional Unix `user`, optional `resources` (cgroup limits and request quotas)
and a list of `applications`. An application has a `name`, its `domains`, an optional
`root` and `profile`, a `runtime` (`static`, `proxy`, `respond`, `redirect`, `php`,
`node`, `python`, `container`), optional path `routes`, and per-application `cache`,
`security`, `htaccess`, `https_redirect` and `acme` settings.

In `domains`, `*.example.com` matches one label and `**.example.com` one or more (never `example.com` itself; not with `acme: true`); `"*"` (at most one application per listener) serves the hosts no other domain matches. The lookup order is exact, `*.`, `**.` (longest suffix first), `"*"`. `listeners` names the listeners that serve the application (default: every listener). A domain may be served once per listener; applications with different `listeners` may share a domain. The Kubernetes [ingress controller](https://scalws.com/docs/guide/ingress-controller/) generates configuration with the catch-all domain and route `https_redirect`; its [Gateway API](https://scalws.com/docs/guide/gateway-api/) input generates listener-scoped applications and `**.` domains.

```yaml
tenants:
  - name: acme
    resources: {cpu: "400%", memory: 4GiB, pids: 256}
    applications:
      - name: www
        root: /srv/acme/www
        domains: [example.com, www.example.com]
        acme: true
        runtime: {type: static}
```

The guide is [applications.md](https://scalws.com/docs/guide/applications/); the keys are in the reference pages for
[tenants](https://scalws.com/docs/reference/tenants/), [applications](https://scalws.com/docs/reference/applications/),
[routes](https://scalws.com/docs/reference/routes/) and each runtime (for example
[php](https://scalws.com/docs/reference/runtime-php/)). Site files (`include`, [sites.md](https://scalws.com/docs/sites/)) are a
shorter way to write the same tenants and applications.

### Proxy upstream and backends

A `proxy` runtime sends to one address, to one pool, or to several pools by weight
([load-balancing.md](https://scalws.com/docs/guide/load-balancing/)):

| Field | Type | Default | Description |
|---|---|---|---|
| `upstream` | string or `{pool: name}` | none | `http://host[:port]`, `https://host[:port]` or `unix:/absolute/path`: one address, no retries, no ejection (as before). `{pool: name}`: a pool from [`upstreams`](#upstreams). |
| `backends` | list | `[]` | `{pool, weight}` or `{status, weight}` entries: a weighted split per request across pools, at most 64. `status` (500 to 599) answers that share with the status and a short text body instead of using a pool; it does not count as a pool or as an upstream failure. Weights 0 to 1000 (default 1); `0` sends no traffic; at least one must be above 0. |

`upstream` or `backends` is required, and they are mutually exclusive. An unknown pool
name is an error. `preserve_host`, `connect_timeout`, `response_timeout` and `websocket`
apply to pools as well (the timeouts per attempt). `tls_ca_file` is only for a single
`https://` address (pools have `tls.ca_file`), and `client` is ignored with a warning on
pool routes, which use `server.upstream_client`.

```yaml
tenants:
  - name: acme
    applications:
      - name: api
        domains: [api.example.com]
        runtime:
          type: proxy
          backends:
            - {pool: api-v1, weight: 90}
            - {pool: api-v2, weight: 10}
```

### Route matching

A route chooses its requests by path and, optionally, by method, headers and query
([ADR-0050](https://scalws.com/docs/adr/0050-route-matching-and-policies/)):

| Field | Type | Default | Description |
|---|---|---|---|
| `path` | string | required | The prefix, the exact path or the regular expression, by `path_type`. |
| `path_type` | `prefix`, `exact` or `regex` | `prefix` | `prefix`: whole segments (`/api` matches `/api` and `/api/x`, not `/apikeys`). `exact`: the decoded, normalised path exactly. `regex`: a regular expression anchored at the start of the path and not at the end. |
| `methods` | list of strings | `[]` (any) | Request methods, upper case (`GET`, `POST`, ...). `HEAD` must be listed on its own. At most 16. |
| `headers` | list | `[]` | `{name, value}` or `{name, regex}`: the header must be present with that value (exact, case-sensitive) or a value the regex finds. Names are case-insensitive; a header sent several times matches if any value does. At most 16. |
| `query` | list | `[]` | `{name, value}` or `{name, regex}` on the decoded value of the first occurrence of the query parameter (`+` is not turned into a space). At most 16. |

All matchers of a route must match. Header and query regexes search anywhere in the value
unless they anchor themselves (`^...$`).

**Precedence** within an application:

1. `exact` routes;
2. `prefix` routes, longest path first;
3. `regex` routes, in the order they are written.

Among routes with the same path type and path, the one with a method matcher is
tried first, then the one with more header matchers, then the one with more query
matchers, then the earlier one. Order changes where same-path routes differ in the kind
of matcher: a method list counts once however many methods it names, and header matchers
outrank any number of query matchers. Configurations whose same-path routes use only
header matchers, or only query matchers, keep their order; the routes the ingress
controller generates keep theirs. A route whose matchers do not match is skipped and the
next candidate is tried; when no route matches, the application's `runtime` answers, or
`404` when the application has only routes (`runtime` may then be omitted). Two routes
with the same path type, path and matchers are an error. A prefix route `/` without matchers matches every request, so regex routes after
it are never used (a warning at load).

Paths are matched after normalisation and after `.htaccess` rewrites. When `.htaccess`
rewrote the request, the route the client's own (normalised) path selects keeps its
access policies: the `allow`/`deny` of both routes must pass, and the client route's
`basic_auth` is checked (the selected route's only when the client route has none, since
one `Authorization` header cannot satisfy two users files). So a front controller
(`RewriteRule ^ index.php`) does not take `/admin/users` out of an `/admin` route with
`basic_auth`. `prefix` paths follow the rules in [applications.md](https://scalws.com/docs/guide/applications/#routes)
(an encoded slash, `%2F` in any case, ends a segment there: `/admin%2Fx` selects
`/admin`); `exact` paths start with `/`, are normalised and contain no `?`, `#`, `%`, `\`,
`*` or spaces.

**Regular expressions** (`path_type: regex` and `regex` matchers) use a linear-time engine
without backtracking (the `regex` crate's syntax without look-around or back-references).
They are case-sensitive unless the pattern starts with `(?i)`. Limits, checked at load:

* pattern text at most 4096 bytes, nesting at most 64 levels;
* at most 256 KiB compiled per pattern;
* compiled size plus a 32 KiB search cache per pattern, summed over every regex of the
  configuration, at most 8 MiB.

A regex route's captures are available to its [rewrite](#route-policies) as `$1` to `$9`
(`$0` is the whole match).

```yaml
routes:
  - path: /healthz
    path_type: exact
    runtime: {type: respond, body: "ok\n"}
  - path: /api
    methods: [GET, HEAD]
    headers:
      - {name: x-canary, value: "always"}
    runtime: {type: proxy, upstream: {pool: api-canary}}
  - path: /api
    runtime: {type: proxy, upstream: {pool: api}}
  - path: "/users/([0-9]+)/avatar"
    path_type: regex
    rewrite: {path: "/avatars/$1.png"}
    runtime: {type: static, root: /srv/media}
```

### Route policies

Every route can carry these fields; all are optional. An application gets them for all its
requests through a route `path: /`.

| Field | Type | Default | Description |
|---|---|---|---|
| `rewrite.path` | string | none | New path: `$1` to `$9` from a regex route, `$0` the match, `$$` a literal `$`. The part after a `?` replaces the query; without `?` the query is kept. Must start with `/`; literals are written percent-encoded. |
| `rewrite.prefix` | string | none | Replaces the matched prefix (prefix routes only). Exclusive with `rewrite.path`. |
| `rewrite.host` | string | none | `Host` sent upstream, `host` or `host:port` (implies `preserve_host: false`). Proxy runtimes only; a warning for others. |
| `request_headers` | `{set, add, remove}` | none | Changes to the request headers: `set` replaces, `add` appends, `remove` deletes. Proxies apply them after their `X-Forwarded-*` headers; every other runtime sees them as the request arrives (PHP as `HTTP_*` parameters). |
| `response_headers` | `{set, add, remove}` | none | Changes to every response of the route, including answers generated by ScalWS (`401`, `403`, `413`, `504`, ...). |
| `max_body` | size | `limits.max_request_body_bytes` | Largest request body for the route, above or below the global limit; `413` above. |
| `request_timeout` | duration | none | Time from the route match until the response head; `504` when it passes. The body is not bounded. |
| `https_redirect` | bool | the application's | Answer plain-HTTP requests that select this route with a `301` to HTTPS, or (`false`) serve them over HTTP although the application redirects. The route chosen by the client's own path decides, before `.htaccess`. |
| `cors` | map | none | Cross-origin resource sharing, see below. |
| `basic_auth.realm` | string | `Restricted` | Realm in `WWW-Authenticate` (1 to 128 printable ASCII characters, no `"` or `\`). |
| `basic_auth.users_file` | path | required | Absolute path of an htpasswd file. |
| `allow` | list | `[]` | Addresses and CIDR blocks (IPv4, IPv6); when set, other clients get `403`. At most 1024. |
| `deny` | list | `[]` | Addresses and CIDR blocks refused with `403`. At most 1024. |

Order for a matched route: `allow`/`deny`, CORS preflight, basic authentication,
`max_body`, tenant quotas, rewrite, request headers, runtime, response headers. The
application's `security` (deny paths, rate limits) and `.htaccess` run before the route,
on the path as received; a rewritten path is checked against the deny paths again (`404`).
The full sequence is in [request-processing.md](https://scalws.com/docs/guide/request-processing/#request).

* **Rewrite.** The request is never decoded twice: text copied from the request (the rest
  after the prefix, each capture) is percent-encoded once, configuration literals are taken
  as written, and the result is normalised once. `/app/%252e%252e/x` rewritten with
  `prefix: /v2` becomes `/v2/%252e%252e/x`. A capture that would form a `.` or `..`
  segment, or a result above `/`, answers `400`. Validation refuses `$n` above the regex's
  capture groups, `$` on routes that are not regex routes, and more than one `?`. The
  rewritten path is what the runtime sees (the upstream path, the static file looked up);
  PHP keeps the client's URI in `REQUEST_URI`. In an application with `htaccess: true`,
  `.htaccess` files are read for the client's path before the route's rewrite: the access
  rules of the rewritten path's directories are not applied (a warning at load for
  `rewrite.path` and `rewrite.prefix` on `static` and `php` routes).
* **Header changes.** At most 64 names per block, values at most 8 KiB of printable
  ASCII. Hop-by-hop and framing headers (`connection`, `keep-alive`, `proxy-connection`,
  `proxy-authenticate`, `proxy-authorization`, `te`, `trailer`, `transfer-encoding`,
  `upgrade`, `content-length`) cannot be changed, and `host` is changed with
  `rewrite.host`.
* **CORS.** `allow_origins` (required): origins such as `https://app.example.com`,
  `https://*.example.com` (one or more labels before `example.com`; scheme and port must
  match; no trailing dot on the host), or `["*"]` alone. `allow_methods` (default
  `GET, PUT, POST, DELETE, PATCH, OPTIONS`), `allow_headers` (default `DNT, Keep-Alive, User-Agent, X-Requested-With,
  If-Modified-Since, Cache-Control, Content-Type, Range, Authorization`), `expose_headers`
  (default none), `allow_credentials` (default `false`; refused together with `*`) and
  `max_age` (default `20d`, at most 365 days). A preflight (`OPTIONS` with `Origin` and
  `Access-Control-Request-Method`) is answered with `204` without reaching the runtime.
  Other requests from an allowed origin get `Access-Control-Allow-Origin` (the origin, or
  `*` without credentials). The route owns `Access-Control-Allow-Origin`,
  `-Allow-Credentials` and `-Expose-Headers`: values from the runtime or from
  `response_headers` are replaced or removed. Every response of the route has
  `Vary: Origin`.
* **Basic authentication.** The users file holds `user:hash` lines with bcrypt (`$2a$`,
  `$2b$`, `$2y$`, cost at most 12), SHA-256 or SHA-512 crypt (`$5$`, `$6$`, at most
  100,000 rounds) or Apache MD5 (`$apr1$`, the default of `htpasswd`) hashes. Plain text,
  `{SHA}` and DES crypt entries are refused with a warning and never match. The file is at
  most 1 MiB, read at load and on reload (and watched with `server.reload.watch`); on Linux
  it and its directory must belong to root or to the user scalwsd runs as and must not be
  writable by group or others. Missing or wrong credentials get `401` with
  `WWW-Authenticate: Basic realm="..."`; more than 64 checks at once get `503` with
  `Retry-After: 1`. The `Authorization` header is not passed to the runtime.
* **Address lists** use the client address after
  [`trusted_proxies`](#trusted_proxies). A `deny` match is refused even when `allow`
  lists the address.
* Routes with `basic_auth`, `allow` or `deny` never use the response cache
  ([caching.md](https://scalws.com/docs/guide/caching/)). In an application whose routes use `exact` or `regex`
  paths or matchers, cached responses are kept per route: two routes on one URL (a
  canary route selected by a header, say) never share an entry.

```yaml
routes:
  - path: /admin
    allow: ["198.51.100.0/24", "192.0.2.10"]
    basic_auth: {realm: "Admin", users_file: /etc/scalws/auth/admin.htpasswd}
    max_body: 50MiB
    request_timeout: 2m
    runtime: {type: proxy, upstream: {pool: api}}
  - path: /api
    rewrite: {prefix: /v2, host: api.internal}
    request_headers:
      set: {x-env: prod}
      remove: [x-debug]
    response_headers:
      set: {x-frame-options: DENY}
      remove: [server]
    cors:
      allow_origins: ["https://app.example.com", "https://*.example.com"]
      allow_credentials: true
      max_age: 1d
    runtime: {type: proxy, upstream: {pool: api}}
```

### Redirect fields

The `redirect` runtime takes either `to` (an origin, as before) or Gateway-style fields
that replace parts of the request URL:

| Field | Type | Default | Description |
|---|---|---|---|
| `scheme` | `http` or `https` | the request's | Scheme of the target. |
| `host` | string | the request's host | Host of the target. |
| `port` | integer | see below | Port of the target, 1 to 65535. |
| `path` | string | the request's path | Replaces the whole path (starts with `/`, printable ASCII without spaces, `?` and `#`). |
| `prefix` | string | none | Replaces the matched prefix of the path. Only on the runtime of a prefix route or the application's own runtime; exclusive with `path`. |
| `status` | 301, 302, 303, 307 or 308 | `301` | Redirect status. |

`to` cannot be combined with these fields. As in the Gateway API, an unset `port` is the
well-known port of a configured `scheme`, else the request's port; a well-known port (80
for `http`, 443 for `https`) is not written. The query is kept.

```yaml
routes:
  - path: /old-docs
    runtime: {type: redirect, scheme: https, host: docs.example.com, prefix: /docs, status: 308}
```

## Complete production example

A single host serving HTTP and HTTPS with automatic certificates, sites in
`/etc/scalws/sites.d/`, per-client limits and a response cache. Everything not shown keeps
its default.

```yaml
include: sites.d/*.yaml

server:
  listeners:
    - name: http
      address: "0.0.0.0:80"
    - name: https
      address: "0.0.0.0:443"
      strict_sni: true
      tls:
        acme: true
        certificates:                       # optional: a static fallback certificate
          - cert: /etc/scalws/tls/example.com.pem
            key: /etc/scalws/tls/example.com.key
            server_names: [example.com, "*.example.com"]
            default: true

  limits:
    max_request_body_bytes: 64MiB

  metrics:
    address: "127.0.0.1:9900"

  logging:
    format: json
    level: info

  acme:
    enabled: true
    contact: ["mailto:ops@example.com"]
    terms_of_service_agreed: true

  cache:
    memory: 256MiB
    max_entry_size: 8MiB

  security:
    per_ip: {max_connections: 256, requests_per_second: 50, burst: 200}

  # Uncomment behind a load balancer:
  # trusted_proxies: ["192.0.2.0/24"]

  cgroups: {mode: auto}

tenants: []        # sites come from sites.d/*.yaml
```

Check it with `scalwsctl check`, then apply it with `scalwsctl reload` (or start the
service). The full commented reference configuration ships as
`/usr/share/doc/scalws/examples/scalws.yaml`.

## Reload or restart?

Each key's Reload line in the [reference](https://scalws.com/docs/reference/) says whether a change
applies on reload, needs a restart, or is only read when the process starts. In summary:

`scalwsctl reload` (or `systemctl reload scalwsd`) applies most changes without dropping
connections: tenants and applications, routes (with their matchers and policies, and the
`basic_auth` users files), `upstreams`, certificates and server names,
`strict_sni`, limits other than `max_connections` and the two upgraded-connection limits, the values of the per-IP limits,
`trusted_proxies`, logging's `access_log`, `http1_server`, route-level `client`, `reload.watch`,
`reload.interval`, `health.metrics` and `limits.shutdown_delay`.

Every change under `upstreams` applies on reload. Pools are matched by name: a pool whose
settings (everything except `members`) are unchanged keeps its connections and passive
health state, and a file pool's members that stay keep their connections; a pool whose
settings changed is rebuilt. Members set at run time for a `members_from: api` pool are
kept across reloads but not across a restart, after which the pool starts with the file's
`members` (or empty). Per-application rate-limit and tenant quota state is kept across a
reload as long as the limit's definition is unchanged. See
[load-balancing.md](https://scalws.com/docs/guide/load-balancing/#changing-members-at-run-time).

A reload of a single scalwsd process is refused with "configuration change requires a
restart" when any of these change; use `systemctl restart scalwsd`:

* listeners: adding, removing or renaming one, or changing its address, TLS on/off,
  `http3` or `proxy_protocol`;
* the metrics endpoint (`metrics.enabled`, `metrics.address`);
* `limits.max_connections`;
* `cgroups`;
* `cache.memory`, `cache.max_entry_size` or `cache.disk`;
* turning `security.per_ip` on or off, or changing its `table_size`;
* `event_loops`;
* anything under `ai`;
* the admin socket (`admin.enabled`, `admin.socket`) or `admin.remote`;
* the health endpoint (`health.address`).

Some settings are only read when scalwsd starts and are kept across reloads:
`logging.format`, `logging.level`,
`limits.max_upgraded_connections`, `limits.upgraded_idle_timeout`, `optimizer.journal_size`,
`listener_mode`, `listen_socket`, `accept_policy`, `ebpf.network`, `ktls`,
`upstream_client` and `sandbox`. HTTP/3 listeners take their connection limits when they are built, so
for HTTP/3 the `limits` apply after a restart.

Under scalws-controller (`listener_mode: inherited`) a reload starts a new configuration
generation and replaces instances one by one, and listeners can be added, removed or
moved without restarting the controller; see [../adaptive-runtime.md](https://scalws.com/docs/adaptive-runtime/)
and [../listener-generations.md](https://scalws.com/docs/adaptive-runtime/listener-generations/). Day-to-day commands are in
[../OPERATIONS.md](https://scalws.com/docs/operations/).
