backgrounders
Diagnosing Tableau backgrounder delay: reading the queue, not the job
Karan Arora · Founder · 8 min read · Last reviewed:
"The refresh is slow" is the ticket. It is almost never what happened. The refresh — the job itself — usually ran in the same eight minutes it always runs in. What changed is how long it stood in line first. Users experience the sum of queue wait and runtime; most admins, when they investigate, look only at runtime. This guide is about the other half: diagnosing delay by reading the queue, not the job.
The anatomy of a late refresh
Every background job in the Tableau Server repository carries three
timestamps in background_jobs, and delay analysis is the arithmetic
between them:
| Interval | Meaning |
| --- | --- |
| started_at - created_at | Queue wait — the job existed, no backgrounder was free |
| completed_at - started_at | Runtime — the job held a backgrounder |
| completed_at - created_at | What the user experienced |
One clarification about created_at — and this one is observed
repository behavior rather than anything Tableau documents: for a
scheduled task it is when the schedule fired and put the job into the
queue, not when a human decided they wanted the data. The wait you
measure from it is therefore pure platform delay, uncontaminated by
anyone's intentions, which is exactly what makes it attributable: every
minute of it happened after Tableau knew the job existed.
The distinction matters because the two intervals have different causes, different owners, and different fixes. Runtime belongs to the content: the extract's size, the query, the source. Queue wait belongs to the platform: how many jobs were scheduled into the same window, how many backgrounders exist to serve them. A workbook owner can fix a runtime. Only an administrator can fix a queue — which failure modes look like timeouts are often queue problems in disguise.
First: split the blame
One query says which half of the latency budget is being spent where:
SET TRANSACTION READ ONLY;
SELECT
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,
ROUND(PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY
EXTRACT(EPOCH FROM completed_at - started_at)) / 60) AS p50_run_min,
ROUND(PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY
EXTRACT(EPOCH FROM completed_at - started_at)) / 60) AS p95_run_min
FROM background_jobs
WHERE created_at >= NOW() - INTERVAL '30 days'
AND completed_at IS NOT NULL;Percentiles, not averages, and P95 in particular: delay complaints come from the tail. An estate can show a P50 wait of two minutes — perfectly healthy — while the P95 sits at 23 minutes and the worst job waited over three hours. The averages hide exactly the jobs people write tickets about.
The reading is a fork. If waits dwarf runtimes, you have a scheduling or capacity problem and the rest of this guide applies. If runtimes dwarf waits, the queue is innocent: you have fat jobs, and the job-mix and runtime analysis in the backgrounder sizing guide is the right tool. If both are small and users still complain, what they are calling "slow" is usually a refresh schedule that finishes at 9:04 against an 8:00 expectation — a frequency problem, not a performance one.
Second: find where the delay lives
Queue delay is almost never uniform. It concentrates — by hour, by day, by schedule anchor. Bucket the waits by hour of week:
SELECT
TO_CHAR(created_at, 'Dy') AS day,
EXTRACT(HOUR FROM created_at) AS hour,
COUNT(*) AS jobs,
ROUND(AVG(EXTRACT(EPOCH FROM started_at - created_at)) / 60)
AS avg_wait_min,
ROUND(MAX(EXTRACT(EPOCH FROM started_at - created_at)) / 60)
AS worst_wait_min
FROM background_jobs
WHERE created_at >= NOW() - INTERVAL '90 days'
GROUP BY 1, 2
ORDER BY avg_wait_min DESC
LIMIT 20;In the estates we review, the result is rarely a surprise once seen and never a surprise twice: the delay lives in one or two windows — typically the small hours, where nightly refreshes stack, with weekday mornings a distant second. An illustrative pattern from our rule library: 95% of jobs waited under 23 minutes, but the delays clustered around 4 a.m., and the worst wait — 208 minutes — belonged to a job queued at the peak of that pile.
The cluster is the diagnosis. A queue that is slow everywhere points at capacity. A queue that is slow at 4 a.m. and empty at 2 a.m. points at scheduling — demand crammed into a window while adjacent hours sit idle.
Third: find the pile-up itself
Delay clusters form because schedules anchor to the same instants. People pick round times — top of the hour, half past — and defaults compound the habit. Count how many jobs enter the queue in the same minute:
SELECT
DATE_TRUNC('minute', created_at) AS minute,
COUNT(*) AS jobs_queued
FROM background_jobs
WHERE created_at >= NOW() - INTERVAL '30 days'
GROUP BY 1
ORDER BY jobs_queued DESC
LIMIT 20;Look at the minute values in the result, not just the counts. When the
top of this list is all :00 and :30, dozens of jobs are racing for
the same backgrounders at the same instant while the minutes around
them are quiet. A pool of four backgrounders served thirty jobs queued
at 04:00 exactly as you would expect: the first four start, twenty-six
wait, and the last one waits through every runtime in front of it. The
queue was not undersized. It was ambushed.
Two refinements are worth making while you are here. First, separate
the populations: filter the same query by job_name and run it once
for refreshes and once for subscriptions. Morning subscription bursts —
hundreds of rendered views queued at 7:00 for the 7:05 emails — are a
different pile-up with a different fix than the nightly refresh stack,
and mixing them in one count blurs both.
Second, check for serial schedules. A schedule can be configured to run its tasks serially rather than in parallel — as Tableau's job prioritization documentation puts it, "jobs associated with a schedule that is set to run serially run one at a time." A long chain of refreshes on one serial schedule produces a signature that looks bizarre at pool level: jobs queued at the same instant, starting one after another with long gaps, while backgrounders sit visibly idle. No amount of pool capacity fixes it, because the constraint is the schedule's own configuration. If a title's waits look inexplicable against an idle pool, look up which schedule it belongs to before blaming anything else.
How the queue is actually ordered
The same documentation lays out the order in which queued jobs are served, and it is worth internalising because it explains results that otherwise look arbitrary. Jobs already in progress finish first. Then come manually initiated jobs — "any task or schedule that you initiate manually using Run now starts when the next backgrounder process becomes available" — with one documented exception: flow runs are excluded, and use their assigned task priority even when triggered manually. Then priority number decides, 1 being highest and 100 lowest, regardless of how long anything has been waiting. Only then does queue order matter. And for jobs scheduled at the same instant — exactly the same-minute pile-ups this guide keeps finding — the tie is broken by task type speed and historical performance: jobs that have historically run faster start ahead of jobs that have historically run slower.
That last rule is why the ordering inside a 4 a.m. pile-up is not random. The quick jobs jump the queue, and the estate's slowest refreshes — the ones most likely to matter to somebody's morning — are systematically served last within their priority band. If a big morning-critical extract always seems to finish latest, that is the scheduler working as documented, and the fix is a better priority number or an earlier, quieter slot.
Fourth: find who absorbs the delay
Delay is not shared fairly. Some content happens to be scheduled at the worst minute of the worst hour and pays for everyone:
SELECT
title,
COUNT(*) AS jobs,
ROUND(AVG(EXTRACT(EPOCH FROM started_at - created_at)) / 60)
AS avg_wait_min,
ROUND(MAX(EXTRACT(EPOCH FROM started_at - created_at)) / 60)
AS worst_wait_min
FROM background_jobs
WHERE created_at >= NOW() - INTERVAL '30 days'
AND started_at IS NOT NULL
GROUP BY title
HAVING COUNT(*) >= 5
ORDER BY avg_wait_min DESC
LIMIT 20;Cross-reference this list against what the business actually watches in the morning. The expensive outcome is not a big number in a table — it is the CFO's daily pack queued behind forty subscription renders it never needed to compete with. That comparison, wait-by-title against importance-by-title, is where queue diagnosis turns into a plan.
One caution while reading per-title waits: as covered above, manually triggered runs are served ahead of scheduled ones (flows excepted). A title whose scheduled 4 a.m. run waits forty minutes but whose "Run now" tests start instantly is not evidence that the queue is fine — it is evidence that your tests jump it.
The fix ladder
Queue delay has a fix ladder, and the cheap rungs come first:
- De-anchor the schedules. Spread the
:00pile across the empty minutes and hours around it. This is free, invisible to users, and in our reviews it is the single most common fix — the 4 a.m. spike usually dissolves into an untroubled 1-to-6 a.m. spread. - Shorten the fattest runtimes. Every runtime minute removed is a queue minute someone behind that job gets back. The levers — filters, aggregation, incremental refresh — are covered in the sizing guide's job-mix section.
- Sequence by deadline, not by habit. Content needed at 8:00 should queue at 2:00, not 7:00. Refreshes with no morning deadline have no business in the pre-dawn window at all.
- Cut demand that serves nobody. Hourly refreshes on workbooks opened weekly; refreshes on content nobody has opened in a quarter. The queue is shorter when it stops carrying dead weight.
- Then, and only then, capacity. If the waits survive rungs one through four, the sizing arithmetic applies — measured, per how many backgrounders your Server should have. This ladder is also the shape of a measured audit of your deployment: the same diagnosis, run across every queue at once, with the fixes ranked.
Prove the fix worked
Every rung of the ladder shares one virtue: its effect is measurable with the same instrument that found the problem. Re-run the wait percentiles split at the date you made the change:
SELECT
CASE WHEN created_at >= DATE '2026-08-01'
THEN 'after' ELSE 'before' END AS period,
COUNT(*) AS jobs,
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 '60 days'
GROUP BY 1;A schedule change that worked shows up here within days, in a number a non-technical stakeholder can read. One that didn't shows up just as fast — which is its own kind of progress, because the ladder's next rung is right there. This is also the honest way to justify capacity spending when you reach rung five: a before/after that failed to move despite fixed schedules and trimmed runtimes is the strongest case for more backgrounders an administrator can bring to a budget conversation.
Tableau Cloud: same queue, different window
Tableau Cloud queues background jobs the same way; you simply cannot see a repository. The Admin Insights Job Performance data source carries the equivalent timing fields, and the same four steps apply — split wait from runtime, find the cluster, find the pile-up, find who absorbs it. The fix ladder loses its final rung (Tableau owns the pool) and keeps the four that matter, which is consistent with what we find on Server: most backgrounder delay is scheduled into existence, and can be scheduled back out of it.
Treat freshness as a promise, not a hope
The durable outcome of queue diagnosis is not a one-off cleanup. It is a changed definition: "the sales dashboard is fresh by 8:00" stops being a hope and becomes a measurable promise — queue wait plus runtime, measured at P95, with headroom. Miss the promise and the same four queries say which rung of the ladder to visit. That is the difference between administering a queue and being surprised by it.