Skip to content
VizBolt

backgrounders

How many backgrounder processes should your Tableau Server have?

Karan Arora · Founder · 8 min read · Last reviewed:

"How many backgrounders do we need?" is the most argued sizing question in Tableau Server administration, and most of the arguments are opinion: the vendor default, what the last consultant said, what the biggest team demands. It has a measurable answer. Your repository already records every background job with its queue time and runtime — which means capacity, utilization, and delay are queries, not debates.

This guide covers what a backgrounder actually is, the three numbers that size the pool, the SQL that produces them, and the decision rules that follow.

What a backgrounder actually does

The backgrounder is the Tableau Server process that executes background jobs: extract refreshes, subscriptions, flow runs, and the platform's own maintenance tasks. The fact that drives all sizing is documented plainly in Tableau's backgrounder process documentation: "Backgrounder is single-threaded. It can only launch a single job at a time." Four backgrounder processes means at most four background jobs running concurrently — everything else waits in the queue, whatever your CPUs are doing.

The same documentation gives a ceiling: you can add backgrounder instances up to one half the number of cores on the node — four backgrounders on an eight-core machine. That is Tableau's published guidance and the number to start from. For what it's worth, our own rule library — calibrated from observed deployments, not from documentation — tolerates a somewhat higher ratio on nodes with headroom; where we recommend above the documented half-core ceiling, we say so explicitly and show the measurements that justify it. The published ceiling tells you what is supported; it does not tell you what your workload needs, which is what the repository is for.

Sizing questions are therefore demand questions. Three numbers describe demand, and all three come from background_jobs timestamps: started_at - created_at is how long a job waited for a free backgrounder; completed_at - started_at is how long it held one.

Number one: what delay do users actually experience

Queue wait is the symptom users feel — the 8 a.m. dashboard showing yesterday's data because its refresh sat in line. Measure it as a distribution by hour of day, not as an average across the week:

sql
SET TRANSACTION READ ONLY;

SELECT
  EXTRACT(HOUR FROM created_at)      AS hour_of_day,
  COUNT(*)                           AS jobs,
  ROUND(PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY
    EXTRACT(EPOCH FROM started_at - created_at)) / 60) AS p50_wait_min,
  ROUND(PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY
    EXTRACT(EPOCH FROM started_at - created_at)) / 60) AS p95_wait_min
FROM background_jobs
WHERE created_at >= NOW() - INTERVAL '30 days'
  AND started_at IS NOT NULL
GROUP BY 1
ORDER BY 1;

Read the shape before the size. A flat few minutes of P95 wait across the day is a healthy pool. A spike — P95 jumping to forty minutes at 4 a.m. and quiet the rest of the day — is not a capacity shortage; it is a scheduling pile-up, and adding backgrounders to fix it buys hardware for one bad hour. Diagnosing where and why delays cluster is its own discipline, covered in reading the queue, not the job.

Number two: how busy the pool actually is

Capacity per hour is trivial arithmetic: backgrounder count × 60 busy-minutes. Demand per hour is a query:

sql
SELECT
  DATE_TRUNC('hour', started_at)     AS hour,
  COUNT(*)                           AS jobs,
  ROUND(SUM(EXTRACT(EPOCH FROM completed_at - started_at)) / 60)
                                     AS busy_minutes
FROM background_jobs
WHERE started_at >= NOW() - INTERVAL '30 days'
  AND completed_at IS NOT NULL
GROUP BY 1
ORDER BY busy_minutes DESC
LIMIT 24;

With four backgrounders, an hour has 240 busy-minutes of capacity. If your worst hours show 90, the pool is idle even at peak and the sizing conversation is over. If the worst hours press against the ceiling — 220, 235, 238 — jobs are necessarily queuing, and you are reading the cause of the waits from number one.

Number three: how deep the concurrency actually goes

Busy-minutes can hide bursts: an hour at half capacity can still contain ten minutes where every backgrounder was occupied. Peak concurrency is an event-sweep query — +1 when a job starts, −1 when it finishes, running sum over time:

sql
WITH events AS (
  SELECT started_at AS ts, +1 AS delta
  FROM background_jobs
  WHERE started_at >= NOW() - INTERVAL '30 days'
  UNION ALL
  SELECT completed_at, -1
  FROM background_jobs
  WHERE started_at >= NOW() - INTERVAL '30 days'
    AND completed_at IS NOT NULL
)
SELECT
  ts,
  SUM(delta) OVER (ORDER BY ts, delta ASC
                   ROWS UNBOUNDED PRECEDING) AS jobs_running
FROM events
ORDER BY jobs_running DESC
LIMIT 20;

Ordering ties with delta ASC closes a finishing job before opening a starting one at the same instant, so back-to-back jobs don't inflate the count. If jobs_running pins at your backgrounder count for sustained stretches — the plateau exactly equalling the pool size is the tell — demand is being clipped by capacity, and the queue-wait spikes in number one will line up with those stretches to the minute.

What the pool is actually spending its time on

Before deciding the pool is too small, look at what it is doing. The backgrounder runs more than refreshes, and the mix matters:

sql
SELECT
  job_name,
  COUNT(*)                            AS jobs,
  ROUND(SUM(EXTRACT(EPOCH FROM completed_at - started_at))
    / 3600)                           AS busy_hours,
  ROUND(AVG(EXTRACT(EPOCH FROM completed_at - started_at)))
                                      AS avg_runtime_s
FROM background_jobs
WHERE started_at >= NOW() - INTERVAL '30 days'
  AND completed_at IS NOT NULL
GROUP BY job_name
ORDER BY busy_hours DESC;

Estates are routinely surprised here. Subscriptions that render hundreds of views every morning, maintenance jobs nobody remembers scheduling, flows that migrated in during a proof of concept and stayed — all of it competes with the refreshes users are actually waiting for. Capacity spent on jobs nobody would defend is capacity you can reclaim without buying anything.

Runtime belongs in the same look. Concurrent demand is arrival rate multiplied by how long each job holds a backgrounder, so runtimes drive the pool size just as hard as job counts do: a hundred five-minute refreshes need less concurrency than twenty ninety-minute ones. The fattest refreshes in avg_runtime_s are sizing levers — filter them, aggregate them, convert them to incremental — and each hour of runtime removed is an hour of capacity added, at zero cost.

Reading a real shape

An illustrative composite from our rule library shows how the three numbers work together. A four-backgrounder estate: P95 queue wait sits under ten minutes for twenty hours of the day, but spikes past forty minutes between 3 and 5 a.m. Busy-minutes tell the second half: 228 of 240 in that window, under 60 everywhere else. Concurrency confirms it — jobs_running pinned at four for over an hour each night.

That is rule 2, not rule 3: a nightly pile-up, not an undersized pool. The schedule audit finds the usual cause — dozens of refreshes anchored to the same top-of-the-hour slots out of habit. Spreading them across the 1–6 a.m. window flattens the spike; the pool that looked saturated at 4 a.m. was idle at 2. Re-measure a month later, and only if the waits survive the rescheduling does the arithmetic of rule 3 come into play — now with evidence that the demand is real rather than self-inflicted.

The decision rules

The three numbers combine into rules that end most sizing debates:

  1. Waits low, busy-minutes low. The pool is fine. Revisit after the next wave of content, not before.
  2. Waits spike in one window, busy-minutes low elsewhere. A scheduling problem. Spread the pile-up across adjacent hours before adding anything — estates accumulate 8 a.m. and top-of-the-hour schedules the way inboxes accumulate subscriptions, and re-timing them is free.
  3. Waits high broadly, busy-minutes near capacity, concurrency plateaus at pool size. A genuine capacity shortage, and the same numbers size the fix: if peak sustained demand is seven concurrent jobs on a pool of four, the arithmetic — not the debate — says what to add. Add headroom for growth, not for the worst minute ever recorded.
  4. Adding backgrounders made things worse. Each backgrounder needs core and memory headroom. A pool that outgrows its node trades queue waits for slower runtimes and out-of-memory failures — the failure modes covered in why extract refreshes fail. Scale the node with the pool, or split backgrounders onto a dedicated node.

Two workload notes worth checking while you are in the data. First, subscriptions share the pool with refreshes: a burst of Monday-morning subscription jobs competes with the refreshes users are waiting on, and job_name lets you separate the two populations in every query above. Second, backgrounder node roles — restricting which job types a node's backgrounders run — have existed since Tableau Server 2019.1, but the real constraint is licensing, not version: the extract-refresh and subscription node roles require the Advanced Management add-on, and the flow roles require Data Management with Prep Conductor. Plan the license before you plan the topology.

When the answer is a node, not a number

One configuration deserves its own warning — and to be clear about the source, this one is observed from our reviews rather than from Tableau's documentation: backgrounders sharing a node with VizQL Server, the process that renders dashboards for live users. Both are hungry, and they are hungry at overlapping times — the morning refresh tail is still running when the first users load dashboards. On a shared node, a saturated backgrounder pool doesn't just delay refreshes; it steals cycles from interactive rendering, and users experience it as slow dashboards with no obvious cause. If your worst queue-wait hours overlap your first login hours, check what else lives on the backgrounder node before adding a fifth process to it.

That is why rule 3's endgame on larger estates is usually a dedicated backgrounder node: background work scales independently, interactive performance stops competing with it, and the busy-minutes arithmetic gets a clean ceiling of its own. The repository numbers make the case either way — a dedicated node justified by measured saturation is an easy conversation; one justified by a hunch is not.

Tableau Cloud: the same question, inverted

On Tableau Cloud you cannot add backgrounder processes — Tableau manages the pool. The demand side of the analysis still applies, from the Admin Insights Job Performance data source: the same queue waits, the same clustering, the same repeat offenders. The levers that remain are the ones rules 2 and 4 use anyway — schedule shaping, extract hygiene, and retiring refreshes nobody reads. In practice that is most of the fix on Server too; the hardware conversation was never the interesting half.

One Cloud-specific wrinkle: refreshes against private-network sources run through Tableau Bridge, whose capacity is your own — Bridge clients pooled on machines you provision. A Cloud estate with slow refreshes sometimes has a perfectly healthy Tableau-managed pool and a starved Bridge pool. The same demand analysis applies; it just points at a different set of machines.

Measure, then decide

If the numbers point at rule 3 and the meeting still wants a second opinion, an independent audit of the whole environment settles it — the same arithmetic, run across every queue, schedule, and node you have.

The backgrounder question stops being contentious the moment it becomes empirical. Three queries, thirty days of history, and the answer is usually one of: leave it alone, move the schedules, or add a known number of processes to a node that can hold them. All three outcomes are cheaper than the meeting where people guess.