skip to content

A connection's exact totalCount times out on large filters; what do you offer clients instead?

level: seniorimportance: should knowfreq 42%

answer

  1. Three different demands hide in one word
  2. The page read already answers one of them
  3. Bound the work instead of the data
  4. A different number needs a different name
  5. Exactness can be late instead of absent

basics

~20 s

Start from what the UI needs. Infinite scroll needs only hasNextPage, which the page read already provides. Otherwise offer a capped count, a clearly named estimate, or a count delivered separately so it never blocks the page.

solid answer

~50 s

First separate the demands hidden inside "we need a total": *is there more* (a boolean), *roughly how many* (an estimate), and *exactly how many* (the expensive one). Infinite scroll and load-more need only the first, and the connection already carries it — `hasNextPage` comes from fetching one row beyond the page, at no extra cost. For a headline number, a **capped** count answers "10,000+" by counting at most cap+1 matches, which bounds the work regardless of corpus size. For scale indicators, an **estimate** from table statistics, a maintained counter or a search index's estimated hit count is orders of magnitude cheaper — name the field so callers cannot mistake it for exact. If an exact number is genuinely required, take it off the critical path: a separate operation, or incremental delivery with `@defer` so the page renders first. Reserve exactness for filters cheap enough to count.

code

graphql · 14 lines
graphql
type TranslationUnitConnection {
  edges: [TranslationUnitEdge!]!
  pageInfo: PageInfo!

  """Exact match count. Null when it exceeds its time budget."""
  totalCount: Int

  """Match count tallied up to 10000; countIsCapped is true if the cap was reached."""
  cappedTotalCount: Int!
  countIsCapped: Boolean!

  """Statistical estimate. Never treat as exact; may drift by several percent."""
  approximateTotalCount: Int!
}

go deeper

for a junior

Know that a list showing "load more" needs only a has-more boolean, and that the server gets it by reading one row beyond the page rather than by counting matches.

for a middle

Explain how a capped count bounds work at the ceiling instead of the corpus, and why an estimate must be exposed under a different field name than an exact count.

for a senior

Show you would interrogate the requirement first, then pick per filter: precomputed where possible, capped or estimated elsewhere, deferred or cached when exactness is unavoidable, with a nullable field as the escape hatch.

for a principal

Own the policy rather than the trick. Decide which count grades exist in your schemas at all, what each is allowed to promise, and how usage data justifies retiring an exact count nobody reads.

## Ask what the number is for "The connection needs a total" is three different requirements wearing one name, and the cheapest correct answer depends entirely on which one is meant. 1. **Is there more?** — needed by infinite scroll, load-more buttons, and any "you have reached the end" state. 2. **Roughly how many?** — needed by "about 218,000 matches" headlines, facet counts, and progress indicators. 3. **Exactly how many?** — needed by numbered page controls that must render the last page number, by reconciliation and export flows, and by anything a person will treat as a figure of record. Most requests turn out to be the first. The connection convention already answers it, and the answer is free: to serve a page of 25 the server reads 26 rows, and the presence or absence of the 26th sets `hasNextPage`. Nothing is counted. In a translation-memory graph whose German fuzzy slice matches 218,431 units, a load-more list can page from the first unit to the last without ever running a count. ## Capping: bound the work, change the meaning If the product wants a headline number and can accept "10,000+", cap the count. Instead of tallying every match, the server counts matches up to `cap + 1` and stops: ``` SELECT count(*) FROM ( SELECT 1 FROM translation_units WHERE project_id = ? AND target_locale = ? AND status = 'FUZZY' LIMIT 10001 ) t ``` The cost is now bounded by the cap, not by the corpus: 10,001 entries visited whether the filter matches 218,431 rows or 4,318,772. On the workload above this turns roughly 1,447 ms into roughly 61 ms, and it stays 61 ms as the project grows. The important part is that capping **changes the field's meaning**, and the schema must say so. A bare `Int` that silently means "10,000 when the truth is 218,431" will be rendered as an exact figure by some client and will be wrong. Either expose the cap explicitly — a count field paired with a boolean saying the cap was hit — or name the field so no one mistakes it. Whatever you choose, the description must state the ceiling. ## Estimating: change the source, not just the limit When even a capped count is not worth the read, estimate. Three sources are common: the storage engine's own planner statistics for a cheap unfiltered or lightly filtered total; a **maintained counter** updated as rows are written, which is exact-ish but drifts and must be reconciled; and a search or analytics index that returns an estimated hit count as a by-product of the query it already ran. All are cheap; none is exact. The discipline is naming. Reusing `totalCount` for an estimate is the mistake, because the convention has trained every consumer to read that name as exact. Give the estimate its own field name, say so in the description, and — if the client needs to render it honestly — consider returning a bucket or a lower bound rather than a precise-looking integer that is wrong in its last four digits. ## Getting the count off the critical path Sometimes the number really must be exact and really is expensive. Then the goal shifts from making it cheap to making it not block anything. - **Let the client ask separately.** Because the count is a field, the client can send the page operation now and a count-only operation alongside it, rendering the list immediately and the headline when it arrives. - **Use incremental delivery.** Where `@defer` is supported, the client marks the fragment holding the count as deferred, and the server sends the page in the initial payload and the count in a later incremental payload. Same one operation, no blocking. - **Cache the count.** Counts are usually far more cacheable than pages: keyed by the exact filter, with a short TTL, and shared across every user with the same filter and the same authorization scope. A dashboard where thousands of users load the same default filter can serve one computed count to all of them. - **Precompute the common ones.** The unfiltered project total, and any handful of filters the UI offers as fixed tabs, can be maintained as counters or refreshed periodically. Reserve live counting for arbitrary user-composed filters. ## What to actually ship A defensible design for this leaf looks like: `hasNextPage` for navigation, always; an exact `totalCount` only where the filter is cheap to count or the result is precomputed; a capped or explicitly named estimate elsewhere; the field nullable so the resolver can decline under a time budget rather than take the whole connection down; and a description on every one of these saying which flavour it is. Then instrument it — field-level timing on the count resolver, and usage counts per client — because the strongest argument for retiring an exact count is usually the discovery that two clients select it and one of them ignores the value.

  • The product owner insists on numbered page controls, which need the last page number. What do you propose?
    Numbered controls are the one UI that genuinely needs an exact total, so either pay for it or change the control. I would first check whether the filters behind it are few and fixed enough to precompute; if they are arbitrary, offer capped pagination — numbered pages up to a ceiling, then "more" — which is what most large search UIs do. If exactness is non-negotiable, cache the count per filter and defer it so it never blocks the first page.
  • Why is a count often far more cacheable than the page it accompanies?
    It is one integer keyed by the filter and the authorization scope, with no cursor in the key, so every user with the same filter shares one entry and a short TTL covers a large burst. A page's cache key includes the cursor and its payload includes the rows themselves, so hit rates are lower and entries are larger. Counts also tolerate staleness better: a few seconds' drift in a headline is invisible, whereas a stale page shows wrong rows.
  • Is returning null from a nullable totalCount when the budget is exceeded better than raising a field error?
    Usually yes, when the client can render without it. Null is a value the client can branch on to show a bare "load more", and it leaves the page intact. A field error also nulls the field but adds an entry to the errors array, which is right when the failure is genuinely unexpected. Whichever you choose, be consistent and document it, so a client cannot read null as zero.

A supermarket does not count every item in the store to tell you the aisle has more; it looks at whether the shelf behind the front row is empty.

saying these in an interview costs you the question

  • Treating every list requirement as needing an exact total
  • Reusing the name totalCount for an estimate
  • Capping the count without exposing that it was capped
  • Assuming caching a page also caches its count
  • Believing hasNextPage requires counting anything
  • Returning zero instead of null when the count is unavailable

context