--- 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: 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" ``` 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" ``` 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" ``` - **`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).