Dashboard shows old data after a successful refresh

Five places to look when a refresh succeeds but the numbers don't move: cache, refresh order, date filters, time zones and silent empty loads.

A successful refresh only means a job ran, not that new rows arrived. Compare the newest timestamp in the source table with what the dashboard shows. If the table is current, look at cache lifetime, the order of refreshes between layers, a date filter that cuts off the newest rows, and a time zone shift. If the table is stale, the load ran and wrote nothing. A freshness check and a daily row count catch the last case without anyone opening the dashboard.

The refresh log says succeeded. The timestamp says a few minutes ago. The chart still ends yesterday, or last Thursday, and someone is asking why.

“Refreshed successfully” means a job finished without an error. It says nothing about whether new rows arrived, whether the layer in front of them noticed, or whether your filter lets them through. Here are five places to look, in the order I’d check them, starting with the one question that splits the problem in half.

First: is the source table current?

Skip the dashboard and ask the table directly:

SELECT max(created_at) AS newest_row,
       now() - max(created_at) AS age
FROM orders;

Use whatever column marks when a row arrived. If you’re unsure which one that is, created_at, updated_at or loaded_at covers the choice.

Two outcomes:

  • The table is current and the dashboard is not. The data is fine. Something between the table and the chart is holding old results: causes 1 to 4 below.
  • The table is stale too. The refresh ran and loaded nothing: cause 5.

Do this first. Teams burn hours clearing caches on a dashboard whose source table stopped updating on Monday.

1. A cache is serving the old result

Most dashboard tools store query results for a while so the same chart doesn’t hit the database on every view. If the cache lifetime is an hour and the data landed ten minutes ago, the chart is correct for the cache and wrong for you.

What to check:

  • The cache lifetime on the query, the dashboard and the connection. They can differ, and the longest one wins in practice.
  • Whether “refresh” on the dashboard reruns the queries or only reloads the page. These are different buttons in many tools.
  • Whether the cached result is shared. A cache keyed by query text serves one person’s stale result to everyone.

A fast test: run the exact query text in the tool’s SQL editor, with caching bypassed if there is an option. If the editor shows today’s rows and the dashboard does not, it’s a cache.

2. Layers refreshed in the wrong order

If the tool sits on top of an extract, a dataset, a dbt model or a materialized view, there are two refreshes: one for the layer and one for what reads it. When the reader runs first, it succeeds against yesterday’s layer, and the layer then succeeds a few minutes later. Both logs are green and the dashboard is one cycle behind, permanently.

A materialized view makes this easy to see, because Postgres doesn’t record when it was last refreshed. Write the time yourself: add now() AS refreshed_at to the view’s query and read max(refreshed_at) from it. Compare that with the refresh time of whatever reads the view. If the reader finished before the layer, you’ve found the bug, and the fix is to chain the refreshes or put the reader after the layer on the same schedule.

3. A date filter cuts off the newest rows

This one hides well because the chart looks complete. A filter like created_at < current_date is common, for good reason: you don’t want a half-finished day. But if the intent was “up to now” and the filter says “before today”, the newest day never appears, and a dashboard that is up to date looks one day behind by design.

Compare what the filter lets through with what the table holds:

SELECT count(*) FILTER (WHERE created_at < current_date) AS rows_dashboard_sees,
       count(*)                                           AS rows_in_table
FROM orders
WHERE created_at >= now() - interval '2 days';

If the two numbers differ by about a day’s worth of rows, the filter is doing it. Decide whether hiding the current day is deliberate. If it is, say so in the chart title (“through yesterday”). A tile that looks stale and isn’t will be reported as a bug every Monday.

Relative date presets are the other source: “last 7 days” in some tools means the seven complete days before today, in others it includes today.

4. A time zone shifts the boundary

Rows are stored in UTC, the dashboard groups by day in the viewer’s zone, or the reverse, and the newest evening disappears into tomorrow’s bucket or into one that isn’t drawn yet. The symptom is a last data point that is low in the morning and fine by afternoon.

Group the same rows both ways and compare:

SELECT (created_at AT TIME ZONE 'UTC')::date           AS day_utc,
       (created_at AT TIME ZONE 'America/New_York')::date AS day_local,
       count(*) AS n
FROM orders
WHERE created_at >= now() - interval '3 days'
GROUP BY 1, 2
ORDER BY 1 DESC, 2 DESC;

If the same events land on different days, pick one zone for the table and the chart and write it down. Timezone pitfalls goes through the traps that cause the most confusion.

5. The refresh succeeded and loaded nothing

If the first query showed a stale table, this is where you are. The job exited cleanly, but the source was empty, a credential expired and the API returned zero rows with a 200, or a filter in the extract matched nothing. An empty load is a valid result as far as the job is concerned, so nothing fails.

You can’t see this from the refresh log. You see it in the data: today has far fewer rows than a normal day.

WITH daily AS (
  SELECT created_at::date AS day, count(*) AS n
  FROM orders
  GROUP BY 1
)
SELECT day, n
FROM daily
WHERE day = current_date
  AND n < 0.5 * (SELECT avg(n)
                 FROM daily
                 WHERE day < current_date
                   AND day >= current_date - 7);

It returns a row only when today is below half the recent daily average, and it returns nothing when things are normal. Run it late enough in the day that a normal day would have rows. For volume that varies by weekday, compare against the same weekday, as in row count anomaly detection in SQL.

Turn the diagnosis into a check

Once you’ve found the cause, the same queries are the monitor. The freshness query from the top and the volume query above each take a threshold, run on a schedule and stay silent until something is wrong. That is how you hear about a stale table before the person who reads the dashboard does. Lightweight data observability for small teams shows how to set up a handful of these without a platform.

If you’d rather not write the SQL for every table and dialect, the free SQL data check generator writes a freshness, volume, duplicate or null check for a table you describe, and it runs in your browser.

Where arcodash fits

I make arcodash, a hosted SQL dashboard tool with scheduled data quality checks. A freshness or volume check is a query with a schedule and a destination, and the alert goes to Slack, email or PagerDuty. How alerts work is the place to start. Start free: 14 days, no card.

More from the blog