Files
langflow/docs/versioned_docs/version-1.10.0/Deployment/deployment-multi-worker.mdx
Mendon Kissling 2411d8036e docs: build OpenAPI spec and cut version 1.10 (#13537)
* build-api

* bump-version-to-1.10

* fix-broken-links
2026-06-08 18:25:14 +00:00

394 lines
17 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: Deploy Langflow with multiple workers
slug: /deployment-multi-worker
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
By default, Langflow runs with a single worker process and stores build job state in memory.
A single-worker process is fine for development, but it doesn't scale when you run more than one worker.
A flow build started on worker **A** cannot be polled or streamed from worker **B** because the in-memory job queue is _per-process_.
A multi-worker deployment runs more than one worker process on the same host. Concurrency can be increased by increasing the number of `LANGFLOW_WORKERS`, but each process keeps its own in-memory build queue unless you add a shared store.
A Redis-backed job queue stores build events in [Redis Streams](https://redis.io/docs/latest/develop/data-types/streams/), so any worker can pick up and serve any job's events.
To configure a multi-worker Langflow process, follow the steps to enable the Redis job queue.
After the Redis job queue is configured, you can optionally follow [recommended Gunicorn settings](#recommended-gunicorn-settings) to reduce memory use and keep workers healthy. This tuning applies to Linux production hosts only. On Windows and macOS, `langflow run` uses a single Uvicorn process.
## Prerequisites
- **Redis 6 or later**, reachable from all Langflow worker processes.
- All workers configured with the same `LANGFLOW_JOB_QUEUE_TYPE`. Mixed-mode deployments (some workers using `asyncio`, others using `redis`) are not supported.
- A dedicated Redis database index for the job queue. The cache uses DB `0` by default; the job queue defaults to DB `1`. Using the same index for both will cause key collisions.
## Enable the Redis job queue
To enable the Redis job queue, set the following environment variables on all workers:
```text
LANGFLOW_WORKERS=3 # any value > 1
LANGFLOW_JOB_QUEUE_TYPE=redis
LANGFLOW_REDIS_QUEUE_URL=redis://your-redis-host:6379/1
```
Redis authentication and TLS are only supported through `LANGFLOW_REDIS_QUEUE_URL`.
The individual host/port settings `LANGFLOW_REDIS_QUEUE_HOST` and `LANGFLOW_REDIS_QUEUE_PORT` create a plain, unauthenticated connection.
If you use a managed Redis service with auth or TLS, you must use `LANGFLOW_REDIS_QUEUE_URL`.
## Example: multi-worker Docker Compose
This example runs three Langflow workers sharing a Redis job queue and a PostgreSQL database.
To run this example you need:
- [Docker](https://docs.docker.com/get-started/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/) (or [Podman Desktop](https://podman-desktop.io/))
- At least 4 GB of memory and 2 CPUs available to the container runtime
:::tip
If you are using Podman Desktop, the default machine might not have enough resources to run this stack. Before starting, increase the machine's CPU and memory allocation:
```bash
podman machine stop
podman machine set --cpus 4 --memory 4096
podman machine start
```
:::
1. Paste the following example into a Docker Compose file named `docker-compose.yml`:
```yaml
services:
langflow:
image: langflowai/langflow:1.10.0
pull_policy: always
ports:
- "7860:7860"
depends_on:
redis:
condition: service_healthy
postgres:
condition: service_started
environment:
- LANGFLOW_DATABASE_URL=postgresql://langflow:langflow@postgres:5432/langflow
- LANGFLOW_CONFIG_DIR=/app/langflow
- LANGFLOW_WORKERS=3 # any value > 1
- LANGFLOW_GUNICORN_PRELOAD=true
- LANGFLOW_JOB_QUEUE_TYPE=redis
- LANGFLOW_REDIS_QUEUE_URL=redis://redis:6379/1
- LANGFLOW_SUPERUSER=admin
- LANGFLOW_SUPERUSER_PASSWORD=changeme
- LANGFLOW_AUTO_LOGIN=False
volumes:
- langflow-data:/app/langflow
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
postgres:
image: postgres:16-trixie
environment:
POSTGRES_USER: langflow
POSTGRES_PASSWORD: langflow
POSTGRES_DB: langflow
volumes:
- langflow-postgres:/var/lib/postgresql/data
volumes:
langflow-postgres:
langflow-data:
```
2. Start the services:
```bash
docker compose up -d
```
3. Watch the logs until Langflow is ready:
```bash
docker compose logs -f langflow
```
These lines confirm a successful multi-worker boot:
```
[preload] initializing services in master
[preload] master preload complete; workers will inherit shared state via COW
✓ Launching Langflow...
```
4. Create a superuser token using the `username` and `password` set in the Docker Compose file:
```bash
TOKEN=$(curl -s -X POST http://localhost:7860/api/v1/login \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=admin&password=changeme" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
```
5. In the same terminal session, verify the Redis queue is active:
```bash
curl -s -H "Authorization: Bearer $TOKEN" \
http://localhost:7860/api/v1/monitor/job_queue | python3 -m json.tool
```
A response looks like the following:
```json
{
"backend": "redis",
"active_jobs": 0,
"bridge_count": 0,
"consumer_wrapper_count": 0,
"background_task_count": 0,
"cancel_dispatcher_running": true,
"cancel_stats": {
"published": 0,
"marker_hit": 0,
"dispatched_owned": 0,
"dispatched_foreign": 0,
"publish_errors": 0,
"dispatcher_reconnects": 0,
"dispatcher_internal_errors": 0,
"polling_watchdog_kills": 0,
"activity_touch_errors": 0,
"activity_get_errors": 0,
"activity_parse_errors": 0
}
}
```
`backend: redis` confirms the queue is using Redis, and `cancel_dispatcher_running: true` confirms the cross-worker cancel channel is active.
6. In the same terminal session, poll the `monitor/job_queue` endpoint:
```bash
while true; do
clear
curl -s -H "Authorization: Bearer $TOKEN" \
http://localhost:7860/api/v1/monitor/job_queue | python3 -m json.tool
sleep 1
done
```
7. Open the Langflow UI and build a flow by sending a message to the flow in the **Playground**.
The number of `active_jobs` reported by the `monitor/job_queue` endpoint increases, confirming your Redis queue is working.
### Verify cross-worker cancellation
To verify that cancellation works across workers, trigger a build with the API to capture the `job_id` and then cancel it.
1. Use the API to build a flow by its `flow_id`:
```bash
curl -s -X POST "http://localhost:7860/api/v1/build/af7dc029-279e-4742-8419-1ac23898afdd/flow" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"inputs": {"input_value": "hello"}, "stream": false}' | python3 -m json.tool
```
Response:
```json
{
"job_id": "1af960e9-12d5-48ec-9860-8a90a16f0b55"
}
```
The returned `job_id` can be used to cancel build jobs in the queue.
2. To cancel a build job, send a request with the `job_id`:
```bash
curl -s -X POST http://localhost:7860/api/v1/build/1af960e9-12d5-48ec-9860-8a90a16f0b55/cancel \
-H "Authorization: Bearer $TOKEN" | python3 -m json.tool
```
Response:
```json
{
"success": true,
"message": "Flow build cancelled successfully"
}
```
3. Confirm the `monitor/job_queue` endpoint reports the cancellation:
```json
{
"backend": "redis",
"active_jobs": 0,
"bridge_count": 0,
"consumer_wrapper_count": 0,
"background_task_count": 0,
"cancel_dispatcher_running": true,
"cancel_stats": {
"published": 1,
"marker_hit": 0,
"dispatched_owned": 0,
"dispatched_foreign": 2,
"publish_errors": 0,
"dispatcher_reconnects": 0,
"dispatcher_internal_errors": 0,
"polling_watchdog_kills": 0,
"activity_touch_errors": 0,
"activity_get_errors": 0,
"activity_parse_errors": 0
}
}
```
`dispatched_foreign` increments when the signal is dispatched to a job owned by a different worker, which confirms the cross-worker cancel path is working.
For more information, see [Monitor the job queue](#monitor-the-job-queue).
## Troubleshoot
See [Troubleshoot multi-worker deployments](./troubleshoot#multi-worker-deployments).
## Configuration reference
| Variable | Default | Description |
|---|---|---|
| `LANGFLOW_JOB_QUEUE_TYPE` | `asyncio` | Job queue backend. Set to `redis` to enable the cross-worker queue. |
| `LANGFLOW_REDIS_QUEUE_URL` | Not set | Full Redis connection URL. Takes priority over `HOST`/`PORT`/`DB` when set. Use this for any Redis instance that requires authentication or TLS. |
| `LANGFLOW_REDIS_QUEUE_HOST` | `LANGFLOW_REDIS_HOST` | Redis host for the job queue. Falls back to the general Redis host setting. Does not support auth or TLS (use `LANGFLOW_REDIS_QUEUE_URL` instead for secured instances). |
| `LANGFLOW_REDIS_QUEUE_PORT` | `LANGFLOW_REDIS_PORT` | Redis port for the job queue. Falls back to the general Redis port setting. |
| `LANGFLOW_REDIS_QUEUE_DB` | `1` | Redis database index for the job queue. Must differ from the cache database index (default `0`) to avoid key collisions. |
| `LANGFLOW_REDIS_QUEUE_TTL` | `3600` | TTL in seconds for job stream and ownership keys in Redis. |
| `LANGFLOW_REDIS_QUEUE_STARTUP_GRACE_S` | `30.0` | Seconds a consumer waits for the producer's first write before treating a missing stream as end-of-stream. Increase this if your workers have slow cold-starts. Setting to `0` removes the grace period so a not-yet-created stream is treated as EOF immediately. |
| `LANGFLOW_REDIS_QUEUE_CANCEL_CHANNEL_ENABLED` | `True` | When `true`, each worker runs a Redis pub/sub dispatcher so that `POST /build/{id}/cancel` cancels a build on any worker, not just the one that received the request. Closing a browser tab also signals cancel cross-worker. |
| `LANGFLOW_REDIS_QUEUE_CANCEL_MARKER_TTL` | `60` | TTL in seconds for the cancel-marker key. The marker closes a race where a cancel signal is published before the target worker's dispatcher has subscribed. A non-positive value is rejected at startup. |
| `LANGFLOW_REDIS_QUEUE_POLLING_STALE_THRESHOLD_S` | `90.0` | Seconds without client activity before the watchdog cancels an abandoned polling build. Set to `0` to disable the watchdog entirely. |
| `LANGFLOW_REDIS_QUEUE_POLLING_WATCHDOG_INTERVAL_S` | `15.0` | How often in seconds the watchdog scans for stale jobs. Lower values reclaim resources faster at the cost of more Redis reads. |
| `LANGFLOW_GUNICORN_PRELOAD` | `False` | **Experimental.** Loads the app in the Gunicorn master process before workers fork, reducing per-worker startup overhead. Pairs well with `LANGFLOW_WORKERS`. Non-Windows only. |
## Monitor the job queue
The `GET /monitor/job_queue` endpoint returns a metrics snapshot for the running worker. It requires superuser authentication and returns HTTP 403 otherwise.
```bash
curl -H "Authorization: Bearer $LANGFLOW_SUPERUSER_TOKEN" \
http://localhost:7860/api/v1/monitor/job_queue
```
Example response for the Redis backend:
```json
{
"backend": "redis",
"active_jobs": 2,
"bridge_count": 1,
"consumer_wrapper_count": 1,
"background_task_count": 0,
"cancel_dispatcher_running": true,
"cancel_stats": {
"published": 5,
"marker_hit": 1,
"dispatched_owned": 3,
"dispatched_foreign": 2,
"publish_errors": 0,
"dispatcher_reconnects": 0,
"dispatcher_internal_errors": 0,
"polling_watchdog_kills": 0,
"activity_touch_errors": 0,
"activity_get_errors": 0,
"activity_parse_errors": 0
}
}
```
For the in-memory (`asyncio`) backend, only `backend` and `active_jobs` are returned.
### Response body
The Redis backend response includes the following fields:
| Field | Description |
|---|---|
| `backend` | The active job queue backend: `redis` or `asyncio`. |
| `active_jobs` | Number of jobs currently owned by this worker. |
| `bridge_count` | Number of active Redis stream bridge tasks on this worker. A bridge reads events from the local `asyncio.Queue` and writes them to Redis Streams so any worker can consume them. |
| `consumer_wrapper_count` | Number of active Redis stream consumer wrappers on this worker. A consumer wrapper reads events from a Redis Stream for cross-worker polling or streaming requests. |
| `background_task_count` | Number of fire-and-forget background tasks currently running (cancel cleanup, marker checks). |
| `cancel_dispatcher_running` | Whether the per-worker Redis pub/sub dispatcher is active. If `false`, this worker cannot receive cross-worker cancel signals. The dispatcher reconnects automatically with exponential backoff (capped at 30s), so a brief `false` during a Redis restart is expected. |
| `cancel_stats.published` | Number of cancel signals published to the Redis pub/sub channel by this worker. |
| `cancel_stats.marker_hit` | Number of times a cancel marker key was found, catching cancels that raced the dispatcher. |
| `cancel_stats.dispatched_owned` | Number of cancel signals dispatched to jobs owned by this worker. |
| `cancel_stats.dispatched_foreign` | Number of cancel signals dispatched to jobs owned by a different worker. A non-zero value confirms cross-worker cancellation is working. |
| `cancel_stats.publish_errors` | Number of Redis errors on the cancel publish path. Persistent non-zero values indicate a Redis connectivity problem. |
| `cancel_stats.dispatcher_reconnects` | Number of times the cancel dispatcher has reconnected after a Redis pub/sub error. |
| `cancel_stats.dispatcher_internal_errors` | Number of unexpected errors inside the cancel dispatcher (not Redis disconnects). Non-zero values indicate a bug; check logs for details. |
| `cancel_stats.polling_watchdog_kills` | Number of abandoned polling builds reclaimed by the watchdog. Non-zero is normal under load. A very high count may indicate frequent client disconnects (consider increasing `LANGFLOW_REDIS_QUEUE_POLLING_STALE_THRESHOLD_S`). |
| `cancel_stats.activity_touch_errors` | Number of errors writing the client heartbeat key to Redis. |
| `cancel_stats.activity_get_errors` | Number of errors reading the client heartbeat key from Redis. |
| `cancel_stats.activity_parse_errors` | Number of malformed heartbeat values encountered by the watchdog. |
## Recommended Gunicorn settings
:::tip
This tuning is optional, and it applies to Linux only. It does not replace the Redis job queue.
On Windows and macOS, `langflow run` uses one Uvicorn process, so `LANGFLOW_GUNICORN_PRELOAD` and `GUNICORN_CMD_ARGS` have no effect.
:::
These recommendations are starting points from Langflow engineering benchmarks on Linux multi-worker deployments.
After you apply these values, monitor performance, and then make adjustments using `htop` or `btop` while you run flows.
Add the starter values to your server's `.env` file after the [Redis job queue](#enable-the-redis-job-queue) settings, then restart Langflow. Copy the block that best matches your server:
<Tabs>
<TabItem value="small" label="Small / dev (48 GB RAM)" default>
Start with fewer workers so Langflow and local databases do not hit OOM.
```text
LANGFLOW_WORKERS=5
LANGFLOW_WORKER_TIMEOUT=120
LANGFLOW_GUNICORN_PRELOAD=true
GUNICORN_CMD_ARGS="--max-requests 100 --max-requests-jitter 20"
```
</TabItem>
<TabItem value="standard" label="Standard API (12 GB RAM)">
A balanced default for a dedicated Langflow host.
```text
LANGFLOW_WORKERS=15
LANGFLOW_WORKER_TIMEOUT=300
LANGFLOW_GUNICORN_PRELOAD=true
GUNICORN_CMD_ARGS="--max-requests 250 --max-requests-jitter 50"
```
</TabItem>
<TabItem value="heavy" label="Heavy multi-agent (24 GB+ RAM)">
Agent loops use more RAM per request, so a lower `--max-requests` value restarts workers more often to keep long-term usage consistent.
```text
LANGFLOW_WORKERS=30
LANGFLOW_WORKER_TIMEOUT=600
LANGFLOW_GUNICORN_PRELOAD=true
GUNICORN_CMD_ARGS="--max-requests 150 --max-requests-jitter 30"
```
</TabItem>
</Tabs>
- **`LANGFLOW_WORKERS`** — How many Gunicorn worker processes run for concurrency.
- **`LANGFLOW_WORKER_TIMEOUT`** — How long a worker may handle a single request before Gunicorn kills it. Raise it if you expect long agent runs.
- **`LANGFLOW_GUNICORN_PRELOAD`** — Loads the app once in the Gunicorn master before workers fork so Linux can share memory across workers through Copy-on-Write. Recommended to leave enabled in multi-worker deployments for memory savings. Safe to leave off; behavior matches older releases when `false`.
- **`GUNICORN_CMD_ARGS`** — Recycles workers after a set number of requests so memory usage growth does not continuously accumulate. `--max-requests` restarts a worker after it processes that many requests; `--max-requests-jitter` adds a random extra 0N requests on top of that limit for each worker. Spreading restarts over time avoids every worker reloading at once. If RAM usage increases over time, lower `--max-requests` before you lower worker count.
For more information, see the [Scaling Langflow blog post](https://www.langflow.org/blog/scaling-langflow).