Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions docs/getting-started/advanced-topics/scaling.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,49 @@ This spawns multiple application processes inside a single container. You still
Container orchestration is generally preferred because it provides automatic restarts, rolling updates, and more granular resource control. Multiple workers inside a single container is a simpler alternative when orchestration isn't available.
:::

### Offload HTTP Compression to the Load Balancer

Once a load balancer, ingress, or CDN sits in front of Open WebUI, let **it** handle HTTP response compression and disable the application-level compression middleware:

```
ENABLE_COMPRESSION_MIDDLEWARE=false
```

By default every Open WebUI worker compresses its own HTTP responses (JSON API responses and static assets) with ZStd/Brotli/Gzip. Profiling shows this costs roughly **3–4% CPU per worker** — multiplied across every replica in a scaled deployment. Enabling compression at the proxy layer instead (e.g. Nginx `gzip on;`, Traefik's compress middleware, Cloudflare's default compression) keeps responses just as small on the wire while freeing that CPU on every worker, and lets CDNs cache static assets in pre-compressed form.

WebSocket traffic and streaming chat responses (SSE) are never compressed by this middleware anyway, so disabling it has no effect on the chat streaming path. If nothing in front of Open WebUI compresses responses, the main cost of disabling is a larger first (uncached) page load — several megabytes of JavaScript/CSS — and larger big-JSON payloads (long chat histories, large model lists), which matters mostly on slow or mobile links. See [`ENABLE_COMPRESSION_MIDDLEWARE`](/reference/env-configuration#enable_compression_middleware) for the full trade-off discussion.

#### Pair It with Static Asset Caching at the Proxy

Disabling app-side compression works best when the proxy also **caches the static assets aggressively**, so the "larger first page load" downside effectively disappears: each browser downloads the (proxy-compressed) bundles once and then never asks for them again.

Open WebUI's frontend is a SvelteKit app: all of its JavaScript/CSS lives under `/_app/immutable/` with **content-hashed filenames**. A given URL never changes content — an upgrade produces new filenames — so these files are safe to cache essentially forever. The HTML shell and `/_app/version.json` are the opposite: they must stay short-lived, because they are how browsers discover a new build (Open WebUI polls `version.json` to detect upgrades and reload).

Example for Nginx:

```nginx
proxy_cache_path /var/cache/nginx/openwebui levels=1:2 keys_zone=OPENWEBUI_STATIC:10m
max_size=1g inactive=7d use_temp_path=off;

# Content-hashed SvelteKit bundles — immutable by construction, cache "forever"
location ^~ /_app/immutable/ {
proxy_pass http://openwebui;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Connection "";
proxy_buffering on;
proxy_cache OPENWEBUI_STATIC;
proxy_cache_valid 200 7d;
proxy_cache_lock on;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
```

- The `immutable` keyword stops browsers from revalidating on reload/F5 — `max-age` alone doesn't. If a year feels uncomfortable, 30 days (`max-age=2592000, immutable`) gives nearly the same effect; the hashed filenames make staleness impossible either way.
- **Do not** apply long caching to the HTML shell or `/_app/version.json` — leave those uncached or at a few minutes at most, or users won't pick up upgrades.
- `proxy_cache` means each asset is fetched from a worker once per cache lifetime instead of once per user, removing the static file serving load from the Python workers entirely.
- If Nginx compresses on the fly, note that it compresses on **every response** (its proxy cache stores the uncompressed body), so prefer moderate levels — `gzip_comp_level 4;` / brotli quality 4–5 gets ~95% of the ratio of level 6 at roughly half the CPU — and set `gzip_min_length 1000;` so tiny responses skip the compressor.

---

## Step 4: Switch to an External Vector Database
Expand Down Expand Up @@ -405,6 +448,10 @@ ENABLE_DB_MIGRATIONS=false
# Concurrency & DB write throttling (REQUIRED at scale — see note below)
THREAD_POOL_SIZE=2000
DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL=300

# HTTP compression — disable in the app IF your LB/ingress/CDN compresses
# responses instead (saves ~3-4% CPU on every worker; see Step 3)
# ENABLE_COMPRESSION_MIDDLEWARE=false
```

:::warning Two settings people forget, and then their scaled deployment stalls
Expand Down
17 changes: 16 additions & 1 deletion docs/reference/env-configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -818,7 +818,22 @@ WEBUI_BANNERS="[{\"id\": \"1\", \"type\": \"warning\", \"title\": \"Your message

- Type: `bool`
- Default: `True`
- Description: Enables gzip compression middleware for HTTP responses, reducing bandwidth usage and improving load times.
- Description: Enables the HTTP response compression middleware ([starlette-compress](https://pypi.org/project/starlette-compress/)), which compresses HTTP responses using ZStd, Brotli, or Gzip, negotiated via the client's `Accept-Encoding` header. This variable is read once at startup and requires a restart to change.

**What it affects — and what it doesn't:**

- Only **HTTP responses** with compressible content types (JSON API responses, HTML, JavaScript/CSS static assets, SVG, etc.) that are larger than 500 bytes get compressed.
- **WebSocket traffic (Socket.IO) is never touched** by this middleware, regardless of this setting.
- **Streaming chat responses (SSE, `text/event-stream`) are never compressed** either way — the content type is not in the middleware's compressible list, so the token-streaming hot path is unaffected by this setting in both directions.

**Trade-offs of disabling (`ENABLE_COMPRESSION_MIDDLEWARE=false`):**

- **Upside**: Removes compression work from the response path of every worker. CPU profiling (py-spy) of production deployments has shown roughly **3–4% of worker CPU time** spent in this middleware; disabling it frees that CPU and slightly reduces server-side response latency. Actual savings depend on your workload (how many large JSON responses and static assets your instance serves).
- **Downside**: Responses leave the backend uncompressed. For typical JSON API responses this only adds a few kilobytes per request, which is negligible on almost any connection. Two cases grow substantially more, however: the **first (uncached) page load** of the web UI, whose JavaScript/CSS bundles amount to several megabytes uncompressed, and **large JSON payloads** such as long chat histories or large model lists, which typically compress 5–10×. On fast or local networks this is still negligible; on slow or mobile links it is noticeable.

:::tip Best of both worlds: compress at the reverse proxy
If a reverse proxy, load balancer, ingress, or CDN (Nginx, Caddy, Traefik, Cloudflare, ...) sits in front of Open WebUI, enable compression **there** and set `ENABLE_COMPRESSION_MIDDLEWARE=false`. Clients still receive compressed responses, while the compression work moves off the Python workers onto infrastructure built for it — CDNs and proxies can additionally cache static assets in pre-compressed form. Pair this with aggressive proxy-side caching of the content-hashed frontend bundles under `/_app/immutable/` (`Cache-Control: public, max-age=31536000, immutable`), which removes the larger-first-page-load downside for every visit after the first. This is the recommended setup for scaled deployments; see [Scaling Open WebUI](/getting-started/advanced-topics/scaling#pair-it-with-static-asset-caching-at-the-proxy) for details and an Nginx example.
:::

#### `DEFAULT_PROMPT_SUGGESTIONS`

Expand Down
4 changes: 4 additions & 0 deletions docs/troubleshooting/multi-replica.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,10 @@ While Open WebUI is designed to be stateless with proper Redis configuration, en
- **Nginx Ingress:** `nginx.ingress.kubernetes.io/affinity: "cookie"`
- **AWS ALB:** Enable Target Group Stickiness.

### Compress at the Load Balancer, Not in the App

By default each Open WebUI worker compresses its own HTTP responses, which profiling shows costs roughly 3–4% CPU per worker — multiplied across all replicas. In multi-replica deployments there is always a load balancer or ingress in front, so enable compression there and disable it in the app with `ENABLE_COMPRESSION_MIDDLEWARE=false`. WebSocket and SSE streaming traffic is never compressed by this middleware, so chat streaming is unaffected. Pair this with proxy-side caching of the content-hashed static bundles under `/_app/immutable/` so they are served from the proxy cache instead of the workers. See [`ENABLE_COMPRESSION_MIDDLEWARE`](/reference/env-configuration#enable_compression_middleware) and [Scaling → Offload HTTP Compression](/getting-started/advanced-topics/scaling#offload-http-compression-to-the-load-balancer).

---

## Related Documentation
Expand Down
15 changes: 15 additions & 0 deletions docs/troubleshooting/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,19 @@ Increasing the chunk size buffers these updates, sending them to the client in l
- **Env Var**: `CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE=7`
* *Recommendation*: Set to **5-10** for high-concurrency instances.

#### HTTP Response Compression
By default, Open WebUI compresses HTTP responses (JSON API responses and the static JS/CSS assets of the web UI) with ZStd/Brotli/Gzip inside the application itself. This costs CPU on every worker: profiling (py-spy) of production deployments shows roughly **3–4% of worker CPU time** spent in the compression middleware. Disabling it frees that CPU and slightly reduces response latency.

* **What it does NOT affect**: WebSocket (Socket.IO) traffic and streaming chat responses (SSE) are **never** compressed by this middleware, so the chat streaming hot path is unaffected either way. Only regular HTTP responses larger than 500 bytes with compressible content types are involved.
* **When to disable**: Your reverse proxy / load balancer / CDN already compresses responses (preferred: enable it there and turn it off in the app), or your users reach the instance over a fast/local network where the extra transfer size doesn't matter.
* **When to keep it on**: The backend is directly internet-facing with nothing in front of it that compresses, and users connect over slow or mobile links. Uncompressed, the first (uncached) page load transfers several megabytes more, and large payloads like long chat histories or big model lists grow 5–10×.

- **Env Var**: `ENABLE_COMPRESSION_MIDDLEWARE=false`

* **Recommended companion**: When you disable app-side compression in favor of the proxy, also have the proxy **cache the static assets aggressively**. Open WebUI's JS/CSS bundles live under `/_app/immutable/` with content-hashed filenames, so they can be cached with `Cache-Control: public, max-age=31536000, immutable` and served from the proxy cache without ever hitting a worker — which eliminates the "larger first page load" downside for every visit after the first. See [Scaling → Pair It with Static Asset Caching at the Proxy](/getting-started/advanced-topics/scaling#pair-it-with-static-asset-caching-at-the-proxy) for a ready-made Nginx snippet.

See [`ENABLE_COMPRESSION_MIDDLEWARE`](/reference/env-configuration#enable_compression_middleware) for the full trade-off discussion.

#### Thread Pool Size
Caps how many **concurrent** blocking operations (sync DB calls, file I/O, sync route handlers offloaded via `run_in_threadpool`) may run at once. This is a concurrency **ceiling**, not a fixed pool of pre-spawned OS threads and **not** a CPU-core/thread count. Threads are created lazily and reused, so a high value does not spawn that many threads, burn CPU, or cause CPU contention while idle.
* **Default**: 40 (the AnyIO default, far too low for production)
Expand Down Expand Up @@ -498,6 +511,7 @@ For multi-user or growing deployments the durable fix is **PostgreSQL**, not SQL
9. **Task Model**: External/Hosted (Offload compute).
10. **Caching**: `ENABLE_BASE_MODELS_CACHE=True`, `MODELS_CACHE_TTL=300`, `ENABLE_QUERIES_CACHE=True`.
11. **Redis**: Single instance with `timeout 1800` and high `maxclients` (10000+). See [Redis Tuning](#redis-tuning) below.
12. **Compression**: `ENABLE_COMPRESSION_MIDDLEWARE=False` **if** your load balancer / ingress / CDN compresses responses (enable it there instead). Saves ~3–4% CPU on every worker. See [HTTP Response Compression](#http-response-compression).

#### Redis Tuning

Expand Down Expand Up @@ -561,6 +575,7 @@ For detailed information on all available variables, see the [Environment Config
| `DATABASE_URL` | [Database URL](/reference/env-configuration#database_url) |
| `ENABLE_REALTIME_CHAT_SAVE` | [Realtime Chat Save](/reference/env-configuration#enable_realtime_chat_save) |
| `CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE` | [Streaming Chunk Size](/reference/env-configuration#chat_response_stream_delta_chunk_size) |
| `ENABLE_COMPRESSION_MIDDLEWARE` | [HTTP Response Compression](/reference/env-configuration#enable_compression_middleware) |
| `THREAD_POOL_SIZE` | [Thread Pool Size](/reference/env-configuration#thread_pool_size) |
| `RAG_EMBEDDING_ENGINE` | [Embedding Engine](/reference/env-configuration#rag_embedding_engine) |
| `CONTENT_EXTRACTION_ENGINE` | [Content Extraction Engine](/reference/env-configuration#content_extraction_engine) |
Expand Down
Loading