Apache Airflow pools, priority weights and concurrency limits

Apache Airflow pools, priority weights and concurrency limits decide how many tasks run in parallel and against which shared resources. This page explains which limit fits which problem and how to set and verify them in infrastructure you control.

On this page

Apache Airflow pools and concurrency limits: when a shared database or API needs protecting

Pools, priority weights and concurrency limits control how many tasks run at the same time and which ones get a slot first. Concurrency limits cap parallel work at the global, DAG and task level. Pools reserve a fixed number of slots for a shared resource, such as a database connection budget or a rate-limited API. Priority weights decide the order in which queued tasks claim free slots. Teams need these controls once several DAGs, often owned by different groups, start at the same time and hit the same system. Without them, a nightly load can open more connections than the database accepts, trigger API throttling and errors, or let one large backfill starve the business-critical pipelines. The failure often shows up as timeouts and retries in tasks that look healthy, which makes the root cause hard to find. These features are part of open-source Apache Airflow, which is released under the Apache License 2.0 and has no paid edition from the project itself. Airflow 3 is the current major line, so the settings described here apply to the Airflow 3 release your team is deploying. Start with an inventory: list every external system that scheduled tasks touch, the maximum concurrency each one tolerates, and which DAGs share it. That list becomes the basis for pool sizes and per-DAG limits. Confirm the current parameter names and defaults against the Airflow documentation for your exact version before you write them into configuration.

The practical work is deciding where each limit belongs. A global limit protects the scheduler and executors from overload, a DAG-level limit keeps one pipeline from flooding the cluster, and a pool with a task-level pool assignment protects a specific resource no matter which DAG calls it. Priority weights resolve contention between tasks that already wait for the same pool. Dynamically mapped tasks need their own check, because one expand() call can create many copies, and max_active_tis_per_dag limits how many run at once across active runs. Use the pool slot metrics, including open, running and queued slots, to confirm that the limits behave as designed under real load. Ask your team to test with a deliberately large run in a staging environment before the limits go to production. For Celery deployments, queues add another layer, since workers only pick up tasks from the queues they listen to. Confirm how your executor, worker pools and any autoscaling interact with these limits, because a pool that is full can make tasks look queued even when workers are idle. Decide who owns each pool, how changes are reviewed, and what alert fires when a shared resource approaches its ceiling. The same decisions shape your cost of ownership: more capacity on the database or API side usually means a bigger bill from that provider, so limits should reflect what those systems can actually absorb.

Which limit applies at which level

Global settings apply across the whole Airflow installation. They cap how much work the scheduler will run at once, regardless of which DAG the work belongs to. Use them as the outer boundary that protects shared infrastructure such as the metadata database, the executor and the worker fleet. Global settings are configured in the Airflow configuration, so changing them usually means a restart of the affected components.

  • `parallelism`: the maximum number of task instances that can run at the same time across the entire installation.
  • `max_active_tasks_per_dag`: the default cap on concurrently running tasks for each DAG, applied when a DAG does not set its own value.
  • `max_active_runs_per_dag`: the default cap on concurrently active DAG runs for each DAG, applied when a DAG does not set its own value.
  • Typical use: keeping one busy team or a burst of new DAGs from exhausting the database connections or worker capacity that every other team shares.
  • Check: confirm the current values in your deployment and whether your team is allowed to change them, since a global change affects every DAG at once.

Before you raise any global value, check the capacity of the database and workers it feeds, and confirm the defaults with the Airflow FAQ.

Concurrency options and when each fits

This table compares the Airflow settings that cap parallel work, from the whole installation down to a single task, and the scope each one covers. Read it top to bottom to find the narrowest control that protects the system you care about. Confirm exact default values and behavior for your Airflow version before you set them in production.

OptionScopeWhat it protectsWhen to choose it
parallelismInstallation-wide (core setting)The total number of task instances the whole installation runs at onceSet as a ceiling for the cluster so one busy DAG cannot starve every other workload
max_active_tasks_per_dagInstallation-wide, applied per DAGEach DAG's share of task slots, so one DAG cannot take all capacityUse when many DAGs share workers and one DAG tends to flood the queue
max_active_runs_per_dagInstallation-wide, applied per DAGThe number of DAG runs that can be active at the same timeUse when overlapping runs would compete for the same source data or downstream windows
max_active_tasksPer DAG (DAG argument)Concurrent task instances within one DAGUse to cap a DAG that is heavy on a shared resource, after checking the installation-wide default
max_active_runsPer DAG (DAG argument)Concurrent runs of a single DAGUse to prevent backfills or catch-up from stacking runs on top of each other
max_active_tis_per_dagPer task (operator argument)Concurrent instances of one task across DAG runs, including mapped copiesUse for a task that calls a rate-limited API or writes to a single table
poolPer task, backed by a named pool with slotsA shared resource such as a database, API or license, across any DAGs that use the poolUse when unrelated DAGs must share one limit. Check pool slot metrics to see whether tasks are waiting
priority_weightPer task (operator argument)The order in which queued tasks receive free slots when capacity is scarceUse to let critical tasks start ahead of bulk work. It orders contention and does not add capacity
queuePer task, routed to a worker queue (Celery)Which workers may run a task, such as tasks that need specific hardware or network accessUse to isolate workloads on separate worker pools. Confirm queue behavior for your executor
max_map_lengthInstallation-wide (core setting)The maximum number of mapped tasks one expand() call can create, 1,024 by defaultLower it to stop a runaway input list from creating a very large task set. Raise it only after checking scheduler and database load

How pools and slot metrics show where tasks wait

A pool is a named count of shared slots that tasks draw from while they run. Each task assigned to a pool takes one or more slots (the number is set per task), so every task using that pool competes for the same budget regardless of which DAG it belongs to. This is what makes pools useful for protecting a database or API: the limit applies across the whole deployment, not just inside one pipeline. The three pool metrics show the state of that budget. pool.open_slots is the capacity still free, pool.running_slots counts slots held by running tasks, and pool.queued_slots counts slots wanted by tasks that are waiting for the pool. Together they tell you whether the pool is the thing holding tasks back.

The key distinction to make when reading these numbers is between a full pool and a queued task. When pool.open_slots falls to zero and pool.queued_slots climbs, tasks are waiting on the pool, and the fix is a design decision: raise the pool size if the downstream system can take more load, or accept the wait. When the pool still has open slots but tasks sit in the queued state, the wait is somewhere else, usually executor or worker capacity, global parallelism limits, or a per-DAG limit. Check the scheduler and executor metrics and the task instance reason before changing any pool. Changing a pool when the real limit is worker capacity changes nothing and hides the diagnosis.

Confirm the exact metric names, tags and available units in the metrics configuration for your Airflow version and your metrics backend (StatsD or OpenTelemetry), because naming can differ between emitters. Before you alert on pool slots, agree with the owners of the protected system what a sustained full pool means for them, and review the metrics page linked from the sources for the current list. Pool metrics are most useful when they are plotted next to the task failure and duration metrics for the same tasks, so a team can see whether waiting tasks are slowing a pipeline or simply queuing harmlessly.

How priority weights and Celery queues change execution order

Once pools and concurrency limits are set, priority weights and queues decide which waiting tasks run first and on which workers. Both settings only matter when work is actually waiting, so the cards below focus on how they interact with slots and capacity.

Priority weight sets the order within a pool

The priority_weight task setting influences which queued task claims a free slot when several tasks wait on the same pool. It does not reserve capacity for one DAG. Confirm in the scheduler documentation how Airflow combines priority weight with weight rules and run dates before you rely on it for ordering across DAGs.

Pools decide which tasks compete

Priority only applies among tasks that share a pool. Tasks in different pools do not compete for the same slots, so a high-priority task in one pool cannot push out work in another. Map each shared database or API to one pool, then review which DAGs share it and where priority values were set.

The queue setting routes tasks to workers

The queue task setting sends a task to a named Celery queue, and only workers listening on that queue pick it up. Use queues to keep heavy or sensitive work on dedicated workers. Confirm the queue names your workers consume in the executor documentation, since a task sent to a queue with no listening worker will wait indefinitely.

Low-priority work can starve

When high-priority or frequently scheduled DAGs keep filling a pool or queue, lower-priority tasks can wait for long periods. Starvation shows up as steady pool.queued_slots with few running slots for those tasks. Set a deliberate priority ordering, watch queue wait times for batch DAGs, and give critical pipelines their own pools rather than relying on weights alone.

Size worker queues against pool limits

A pool limit only helps if enough workers listen on the queues that feed it. If a queue has fewer worker slots than its pools allow, tasks queue even though the pool shows free capacity, and the reverse wastes database connections. Compare the worker_concurrency of each queue with the slot counts you set, and confirm the sizing against your executor and autoscaling configuration.

Setting pools and limits without starving the pipeline

This sequence turns a list of shared resources into pools, DAG limits and global settings that protect databases and APIs while keeping critical pipelines moving. Each step ends with a check you can run before the next one starts.

  1. Inventory shared resources and safe capacity

    Your platform team and the owners of each shared system list every resource that Airflow tasks touch, such as a warehouse connection pool, a rate-limited vendor API, an on-premises database, or an object storage bucket. For each one, record the maximum number of parallel connections or requests the owner will accept, and who to contact when a limit changes. Ask the owners for these numbers in writing rather than inferring them from Airflow. The step is done when every shared resource has a named owner and a documented safe parallel capacity.

  2. Decide the limit level for each resource

    For each resource, decide whether the limit belongs at the global level, the DAG level, the task level, or in a pool. Global settings such as parallelism cover the whole installation, while max_active_tasks and max_active_runs apply per DAG, and pool and max_active_tis_per_dag apply to individual tasks. Shared resources used by many DAGs usually need a pool, because a pool counts slots across every DAG that uses it. The step is done when each resource has one written level and a short reason, reviewed by the platform team.

  3. Create pools and assign tasks

    Data engineers and the platform team create each pool with a slot count equal to the agreed safe capacity, then set the pool argument on the tasks that touch that resource. Give critical, time-sensitive tasks their own pool so that a backfill or a noisy DAG cannot consume every slot. Use priority_weight on tasks inside a shared pool when some work should be scheduled first. The step is done when every task that calls a limited resource names a pool and the pool list matches the inventory.

  4. Set DAG and global limits

    Platform engineers set global parallelism for the installation, then add max_active_runs and max_active_tasks on each DAG so one pipeline cannot start many overlapping runs. For dynamically mapped tasks, set max_active_tis_per_dag and check that [core] max_map_length suits your largest expansion, which is 1,024 by default. Keep the total of all DAG limits in mind, since they compete for the same executor capacity. The step is done when each DAG file declares its limits and the installation-wide values are recorded in your configuration repository.

  5. Deploy and check pool slot metrics

    Deploy the changes through your normal delivery path, then confirm that the metrics pool.open_slots, pool.running_slots and pool.queued_slots appear for every pool in your monitoring system. Trigger a manual run of a DAG that uses each pool and watch running slots rise to the configured count and fall back afterward. A pool that never shows running slots usually means the task is not assigned to it or the DAG is paused. The step is done when every pool reports its metrics and a test run confirms the expected slot movement.

  6. Load-test with representative runs

    Before enabling production schedules, run representative backfills or replayed runs against a staging environment that has the same pool sizes and the same downstream limits. Watch the database connection count, the scheduler heartbeat, the downstream error rate and queue wait time while the runs execute. Adjust a pool size only after the downstream owner confirms the new value is safe. The step is done when the staging run stays inside the agreed downstream limits and the queue wait for critical pools is within the expectation your business owners recorded.

  7. Review limits on a fixed schedule

    Set a recurring review with the resource owners and pipeline owners, using the metrics collected in earlier steps to check whether pools are saturated, idle, or missing tasks. Record each change to a pool size, DAG limit or priority weight in version control with a reason and an approver. Confirm that the review produces a change log that an auditor or incident responder could follow. The step is done when the review cadence, owners and change record process are documented and the first review is complete.

Limiting dynamically mapped tasks with expand()

Set both the size of a mapped fan-out and the number of copies allowed to run at once. A task that calls expand() creates one mapped task instance per item in its input, so a list of 500 customer IDs becomes 500 task instances. Airflow caps the size of a single expansion with [core] max_map_length, which defaults to 1,024 tasks. That cap protects the scheduler and metadata database from a runaway input list, but it does not limit how hard the work hits a downstream system. A fan-out that stays under the default cap can still open hundreds of simultaneous connections to a shared API.

The control for concurrency is max_active_tis_per_dag on the mapped task. It limits how many mapped copies run at once across all active runs of the DAG, not only within one run. This matters when several runs overlap, for example a catch-up backfill running alongside the current schedule, because each run would otherwise get its own allowance. Pools also apply to mapped tasks, so a pool sized for the API can work alongside the per-task cap. Confirm in your Airflow 3.3.2 environment how the two interact for your DAG layout before relying on either alone.

For any fan-out that calls a shared database or third-party API, write down three numbers before the DAG goes to production: the largest expected input length, the length you will accept before failing the run, and the number of simultaneous calls the provider tolerates. Set max_map_length or validate the input list upstream so an unexpected large list fails early instead of being truncated silently. Then set max_active_tis_per_dag to a value below the provider limit, leaving room for other DAGs. Test the setting with a staging list at production size and watch the pool slot metrics while it runs, since the per-task cap alone does not show how much of a shared pool the fan-out is consuming.

Operational risks of misconfigured concurrency

Concurrency limits in Airflow protect shared systems, but a value set too high or too low creates a different failure in each direction. The cards below cover the risks that show up most often in self-hosted deployments and the checks that catch them before a run is lost or a database is saturated.

Overloading database connection pools

Every running task that opens a connection to a warehouse or operational database consumes a slot on that database's connection pool. When parallelism and per-DAG limits allow more concurrent tasks than the database can serve, connections queue, timeouts multiply and unrelated applications slow down. Check the connection limit of each target database, then size the pool or a pool object in Airflow so that the sum of concurrent tasks stays under it. Count connections across every DAG that shares the database, not only one DAG.

Exhausting third-party API quotas

Tasks that call SaaS APIs, payment processors or data vendors can burn through a daily or per-minute quota in minutes when a mapped task fans out. Airflow does not know the quota of an external service unless you tell it through a pool or a task-level limit. Ask each provider for its documented rate limits and any per-key ceilings, then assign those tasks to a dedicated pool sized below the lowest limit. Confirm the provider's terms with the provider, since quota rules vary by plan.

Deadlock-like waits from undersized pools

When a pool has fewer slots than the tasks that depend on it, or a DAG-level limit is held by runs waiting on a shared pool, tasks sit in a queued state with no visible error. The scheduler is working correctly, but the pipeline appears stalled. Compare the pool.queued_slots and pool.running_slots metrics over time, and trace which DAG holds the slots. Review pools after adding DAGs that share them, since a pool that was adequate for one workload can starve the next.

Hidden backlog from limits set too low

A max_active_runs or max_active_tis_per_dag value that is lower than the schedule frequency lets a backlog build without failing a single task. Runs start late, data arrives after downstream consumers expect it, and the delay is easy to miss because each run looks healthy. Compare scheduled run counts against completed runs and alert when queued runs grow for a DAG. Set limits from the actual throughput the upstream and downstream systems can absorb, not from the default.

Limit values changed outside review

Pool sizes, priority weights and max_active_tasks values often live in DAG code, Helm values, environment variables or the Airflow UI, so a change can bypass the review that DAG code receives. A single unreviewed edit can throttle every pipeline or overload a source system. Treat pool and limit changes like DAG code: store them in version control, require the same peer review and test runs, and record who changed which value. Keep the Airflow UI pool editor for emergencies only, and reconcile it back into code afterward.

Questions teams ask before setting Airflow concurrency limits

Can our own team configure Airflow pools and concurrency limits without a partner?

Yes, if you have engineers who know your DAGs and the systems those DAGs call. Pools are created in the Airflow UI or with the CLI, and limits such as max_active_tasks and max_active_runs are set in DAG code or configuration. The harder part is knowing the right numbers. Each limit should reflect what a downstream database, API or file system can absorb, and that knowledge usually sits with the owners of those systems, not with the Airflow team. A capable internal team can do the work; the gap is usually time for testing and measurement, not Airflow knowledge. Airflow is Apache-2.0 licensed, so there is no license cost for using these features.

What drives the effort of setting concurrency limits?

The main drivers are the number of DAGs, how many of them share a resource, the executor you run, and how deeply you test. Effort rises quickly when many DAGs call the same database or API, because every limit interacts with the others. The executor matters because Airflow ships several options, including LocalExecutor, CeleryExecutor and KubernetesExecutor, and each handles slots and queues differently. Dynamically mapped tasks add work, since a single expand() call can create many task instances at once. Testing depth is the final driver: a limit validated only in a quiet staging environment may behave differently under a month-end batch.

How long does tuning concurrency limits take, and what makes it longer?

Duration depends on the scope agreed in writing, so no fixed figure applies here. Ask for a timeline tied to specific deliverables: an inventory of shared resources, proposed pool sizes, a staging test run and a rollout plan. The factors that lengthen the work are a large number of shared downstream systems, owners who are slow to confirm safe limits, a lack of representative test data and a busy production calendar that restricts change windows. Work usually moves faster when the metrics you need are already collected and the staging environment matches production closely enough to reproduce load.

What do we need to provide before the work starts?

Four things make the work go smoothly:

  • Access to Airflow metrics, including the pool slot metrics pool.open_slots, pool.running_slots and pool.queued_slots, and to the scheduler and task failure metrics.
  • A named owner for each downstream system the DAGs call, such as a database, an API or a file store, who can state safe concurrency levels and tell you about maintenance windows.
  • A staging environment with Airflow and representative DAGs, so limits can be tested before production.
  • A list of DAGs, their schedules and their priorities, so that priority weights reflect business importance.

Confirm each item with your own teams before estimating the engagement, because gaps here are the most common cause of delay.

Do pools and priority weights require a paid Airflow feature?

No. Apache Airflow is released under the Apache License 2.0 (SPDX: Apache-2.0), and the project itself offers no paid edition. Pools, priority weights, queues and the global, DAG and task-level limits are part of the open-source software. Costs come from the infrastructure that runs Airflow, the staff who configure and operate it, and any outside help you choose to buy. Confirm with your legal or procurement team how your organization classifies Apache-2.0 software and any third-party tools you add, such as a monitoring stack.

When should we bring in an outside partner for Airflow concurrency work?

Consider a partner when your team lacks time or experience to measure and test limits, when many DAGs share fragile downstream systems, or when a production incident has exposed limits that no one owns. A partner can also help when you are moving from another executor or planning upgrades alongside tuning. DeployOpen plans, implements, tests and documents self-hosted open-source platforms inside your own infrastructure, and most engagements are fixed-scope with scope, deliverables and timelines agreed in writing before work starts. Ask any provider for a written scope that lists the systems covered, the tests performed and the handover material.

Which Airflow version should we tune concurrency on?

Tune on Airflow 3 if you are starting now, because it is the current major line. Airflow 3 was first released on April 22, 2025, and its current patch release is 3.3.2. Airflow 2 reached end of life on April 22, 2026, so it gets no fixes. If you run Airflow 2, check the upgrade guide first: it requires being on Airflow 2.7 or later and recommends moving to the latest 2.x before upgrading. Concurrency settings you tune now should be reviewed again after any upgrade, since defaults and task behavior can change between releases.

Can we verify that a new limit protects a database before production?

Yes, but only with a test that reproduces realistic load. Start by running a representative set of DAGs in staging with the new pool size and watch pool.queued_slots and pool.running_slots alongside the database's own connection and latency metrics. Then test the peak case, such as a backfill or a month-end run, because limits that look safe on normal days can fail under a burst. Ask the owner of the downstream system to confirm the agreed ceiling and to watch for errors during the test. Record the results so the limit can be justified during change review.

Official Airflow documentation for concurrency and metrics

Before you change any concurrency setting, read the official Airflow references side by side with your own deployment. The FAQ entry on concurrency is the most direct starting point, because it lists where each limit lives: global settings such as parallelism, max_active_tasks_per_dag and max_active_runs_per_dag, DAG-level settings such as max_active_tasks and max_active_runs, and task-level settings such as max_active_tis_per_dag, pool, priority_weight and queue. Read it with a specific question in mind, such as which database or API each task calls, so that every limit you set maps to a real downstream capacity. The Airflow FAQ on concurrency is the natural next read after this page.

Once limits are in place, you need to see whether they hold. The metrics configuration reference documents the pool gauges pool.open_slots, pool.running_slots and pool.queued_slots, which show whether tasks are waiting on a pool rather than on the executor. Confirm with your platform team which metrics backend your deployment emits to, because the metric names and tags you will query depend on whether you use StatsD or OpenTelemetry. Also confirm that the pool metrics are not blocked by your metrics allow or block configuration, since a filtered metric looks like a healthy pool.

Dynamically mapped tasks need their own check. The dynamic task mapping guide explains how expand() and partial() create many task instances from one definition, and it sets the default cap of 1,024 tasks per expansion through [core] max_map_length. It also describes how max_active_tis_per_dag limits how many mapped copies run at once across all active runs. Before you raise either value, estimate how many mapped instances a single run can create and confirm how the cap interacts with your database connection budget and pool sizes, because a large expansion can exhaust a shared resource in seconds.

How DeployOpen works on this

DeployOpen plans, builds and hands over concurrency limits for a self-hosted Apache Airflow, starting from the databases, APIs and queues your pipelines must protect. The work is fixed-scope, so limits, metrics and handover deliverables are agreed in writing before any change is made to your environment.

How we scope

  • Inventory which databases, APIs and shared services each pool and DAG call, and their safe parallel limits.
  • Map existing max_active_tasks, max_active_runs and parallelism settings against the workloads they actually throttle.
  • Agree in writing which DAGs, pools, queues and mapped tasks are in scope, and which are excluded.

How we build

  • Define pools and their slot counts in a versioned configuration, with a named owner for each pool.
  • Set priority weights and queue assignments so critical pipelines are not starved by bulk backfills.
  • Cap dynamically mapped tasks with max_active_tis_per_dag and review max_map_length for large expansions.

How we hand over

  • Document each limit, its reason, its owner and the change process for raising it.
  • Provide dashboards and alerts on pool.open_slots, pool.running_slots and pool.queued_slots using your metrics stack.
  • Run a handover walkthrough so your platform team can adjust limits and read the slot metrics confidently.

How we support

  • Review concurrency settings after new DAGs, schedules or database changes that alter load patterns.
  • Available as a retainer for ongoing tuning, upgrades and periodic review of limits against observed queueing.
  • Confirm support boundaries, response expectations and any change windows with your team before the engagement starts.

Talk to our team.

Tell us what you're working on, whether it's a deployment, an audit, a security test or a cyber range. You'll speak with an engineer who can help you scope it.

  • 30-minute call: free, with no obligation.
  • NDA on request: we can sign before you share details.
  • Clear next steps: a scope and plan after the call.