mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-24 16:30:28 +08:00
394 lines
17 KiB
Plaintext
394 lines
17 KiB
Plaintext
---
|
||
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 (4–8 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 0–N 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). |