Where does Airflow store XCom values by default, and what limits their size?
answer
- it lives in Airflow's own database
- not a separate store, a table
- the column type sets the hard ceiling
- serialized as JSON before it is written
- nothing expires until db clean runs
basics
~20 sAirflow serializes each XCom to JSON and writes it as a row in the xcom table of its metadata database. The ceiling is that column's type, so the practical budget is kilobytes to a few megabytes, not gigabytes.
solid answer
~50 sBy default an XCom is a row in the `xcom` table of Airflow's own metadata database, with the value serialized to JSON. Two limits follow from that. The hard one is the column type: the Airflow docs spell out that the value column is a BLOB on MySQL (about 64 KB), BYTEA on PostgreSQL (about 1 GB) and BLOB on SQLite (about 2 GB), so "how big can an XCom be" has a different answer per backend. The soft one matters more in practice: every push and pull is a query against the same database the scheduler uses to make every scheduling decision, so fat XComs turn into scheduler contention, slow UI pages and bloated backups. The table also grows forever unless you run `airflow db clean` on a schedule. Serialization is a limit too — with JSON serialization, anything that is not JSON-serializable raises rather than being stored. Configure a custom XCom backend when you want values offloaded elsewhere.
code
bash · 5 lines# trim history tables, including xcom, older than a cutoff
airflow db clean --clean-before-timestamp 2024-01-01 --yes
# inspect what a task instance actually pushed
airflow tasks states-for-dag-run xcom_demo manual__2024-05-01T00:00:00+00:00go deeper
Know the one-line fact: an XCom is a row in Airflow's metadata database, not a separate store, and it is serialized to JSON on the way in.
Explain that the hard size limit comes from the value column's type on the underlying database engine, and that JSON serialization is itself a constraint on what you can push.
Argue the operational case: fat XComs put load on the same database the scheduler depends on, and the table needs a scheduled db clean or it grows forever.
Own the platform consequence — treating the metadata database as a data store couples every team's payload size to scheduler latency, backup time and restore risk across the whole deployment.
## Where the value actually lands Out of the box, XCom has no separate storage system. A push becomes an `INSERT` into the `xcom` table of the Airflow metadata database — the same PostgreSQL or MySQL instance that holds DAG runs, task instances, logs metadata and the scheduler's own bookkeeping. The row carries the dag id, the run, the task id, the key, the map index and the serialized value. That single design fact explains nearly everything about how XCom behaves and why interviewers keep asking about size. ## Serialization Airflow serializes XCom values to JSON. Practically, that means dicts, lists, strings, numbers and booleans go through cleanly, and anything else — a pandas DataFrame, a database connection, a custom class, a `set` — raises a serialization error when the task finishes rather than being silently stored. Airflow adds handling for a few extra types it knows about, but the mental model to interview with is "if it isn't JSON, it doesn't fit." Older Airflow releases had a pickling switch; do not reach for it, because unpickling attacker-controlled bytes in the scheduler and workers is a security problem, not a size solution. ## The hard ceiling comes from the column The value column is a binary column, and the maximum size is whatever that column type allows on your database engine. The Airflow documentation calls this out explicitly, and the numbers differ per backend: MySQL's BLOB tops out around 64 KB, PostgreSQL's BYTEA around 1 GB, SQLite's BLOB around 2 GB. The consequence is that the *same DAG* can work on one deployment and fail on another. A team that developed against PostgreSQL and deployed onto a MySQL-backed managed Airflow will find tasks failing on the write with a data-too-long error, and the DAG code did not change at all. ## The soft ceiling matters more Even far below the hard limit, large XComs are a bad idea, because the metadata database is a shared, latency-sensitive resource: - **Scheduler contention.** The scheduler queries this database constantly to decide what to queue. Rows measured in megabytes make the table bigger, the indexes colder and the vacuum/maintenance load heavier. - **Every read is a round trip.** A downstream task pulling a 50 MB blob pulls it over the wire from the database, not from a nearby cache. - **The UI renders XComs.** The grid view's XCom tab reads the value; huge values make pages crawl. - **Backups and restores.** Your metadata database backup is now dominated by pipeline payloads that were never meant to be durable. A good rule of thumb to state in an interview: keep XComs in the kilobyte range, treat anything over a megabyte as a design smell, and never let a payload's size scale with the size of the data being processed. ## Lifecycle: nothing expires on its own XCom rows are not TTL'd. They persist for as long as their DAG run's records do. Two mechanisms remove them: - Clearing a task instance deletes the XComs that task instance produced, so a rerun starts clean rather than reading a stale value. - `airflow db clean --clean-before-timestamp <ts>` trims old rows from history tables, including `xcom`. On a busy deployment this is a scheduled maintenance job, not a one-off. A subtle operational consequence: if `db clean` has already removed an old run's XComs and you then clear and rerun a *downstream-only* task in that run, its pull finds nothing. That is one more argument for downstream tasks deriving what they need from the run's logical date rather than depending on a pushed value surviving. ```bash airflow db clean --clean-before-timestamp 2024-01-01 --yes ``` ## The escape hatch If you genuinely want bigger payloads to flow through the XCom API, you change *where* they are stored rather than raising the limit: configure a custom XCom backend, a class that serializes the value into object storage and puts only a small pointer in the database row. That keeps `ti.xcom_pull` looking the same to DAG authors while the bytes live in S3 or GCS. It is a deployment-wide decision, and it brings its own lifecycle problem — deleting the database row does not delete the object. ## What to say when asked "how big can an XCom be?" The strong answer is not a number. It is: "the hard limit depends on the metadata database column type, but I design as if the budget is a few kilobytes, because XCom is a control channel and its cost lands on the scheduler's database."
- The same DAG works on a PostgreSQL-backed Airflow and fails when pushing on a MySQL-backed one. Why?The XCom value column's maximum size follows the database's column type. MySQL's BLOB tops out around 64 KB while PostgreSQL's BYTEA allows far more, so a payload of a few hundred kilobytes writes fine on one backend and raises a data-too-long error on the other. The fix is not a bigger column; it is pushing a pointer instead of the payload.
- What removes old XCom rows from the metadata database?Nothing automatic. Clearing a task instance deletes the XComs it produced, and `airflow db clean --clean-before-timestamp` trims history tables including `xcom`. On a busy deployment that clean runs on a schedule; otherwise the table grows for the life of the installation and drags the scheduler's database with it.
- Why is pickling XCom values not the right answer to "my object isn't JSON-serializable"?It swaps a clear failure for a security and compatibility problem: unpickling means executing arbitrary bytes inside the scheduler and workers, and pickled objects break the moment library versions differ between the pushing and pulling environments. The right answer is to serialize deliberately — write the object to storage and push a URI, or reduce it to the JSON facts downstream actually needs.
saying these in an interview costs you the question
- Thinks XComs live in Redis or on the worker's disk
- Says XComs expire automatically after a run finishes
- Claims there is one fixed size limit on every deployment
- Believes a DataFrame is stored transparently by default
- Sees no problem with megabyte XComs on a busy scheduler