When does a DataLoader dispatch its queued keys to the batch function?
answer
- The key does not leave when you ask
- Something must decide the queue is closed
- Dispatch waits for execution to run dry
- One stall, one dispatch round
- Rounds track dependency depth, not width
basics
~20 sNot when load is called. Each load queues its key and returns a pending result; the queue goes to the batch function only once execution can make no further progress, which may happen several times per request.
solid answer
~50 s`load(key)` starts nothing. It queues the key on that loader and hands back a pending result, so the field that called it parks. Something later decides the queue is complete, and that something is always the same idea: **dispatch when execution can make no further progress**. On a single-threaded event loop the loader schedules its own dispatch for the end of the current frame of execution, so every key queued in that frame ships together. In an engine resolving fields on tasks or threads, the executor makes the call — when every field it has started is parked on a loader and nothing else is runnable, it dispatches all the request's loaders. That happens once per stall, not once per request: a document whose second-level keys come from first-level results gets a dispatch round per dependency level. The mechanism is convention, not specification text.
code
pseudocode · 9 linesresolve Locker.currentCourier(locker, ctx):
return ctx.courierLoader.load(locker.courierId) // queues a key, returns pending, fetches nothing
// what execution does, conceptually
run every field that is reachable // 43 lockers x 12 loader-backed fields = 516 load() calls
// nothing runnable: every started field is parked on a loader
dispatch every loader holding keys // 12 batch-function calls, at most 43 keys each
// results arrive, the 516 parked fields resume, some load again
dispatch again // one more round per dependency levelgo deeper
Remember that asking a loader for a key does not fetch anything yet: the key waits in a queue with the other keys, and they all go out together a moment later. Be able to say why that delay is the point.
Explain the trigger precisely — dispatch happens when execution has nothing else to run — and be able to describe both mechanics, an event loop scheduling the dispatch at the end of a frame and an engine dispatching once every started field is parked on a loader.
Show that you reason about dispatch rounds in production terms: count batch invocations against keys loaded, expect one round per dependency level, and treat a resolver that blocks a worker on a pending result as a hazard to the dispatch pass itself.
Own the policy: which layers of the service are allowed to defer work, whether dispatch behaviour is a property teams may depend on across a fleet of engines, and how loader-round counts become an enforced budget rather than tribal knowledge.
## `load` queues a key; it does not fetch it The first thing to get right about the DataLoader batching pattern is that `load(key)` performs no backend work at all. It does three cheap things: it looks for the key in the loader's per-request memo map, and if the key is absent it appends the key to that loader's pending queue and returns a *pending* result — a promise, a future, whatever the host language calls a value that will arrive later. Then it returns, immediately, so the field that called it is now parked on something nobody has started yet. Batching only works because of that gap. If `load` fetched, there would be nothing to coalesce; each of the 516 calls a single document can make would already be in flight before the second one was issued. The whole technique is buying time: queue everything, then send once. ## What "no further progress" means So something has to decide the queue is complete and hand it over. That decision is always the same in spirit — **dispatch when execution can make no further progress** — and it is reached in two different mechanical ways depending on where the loader runs. On a single-threaded event loop, the loader schedules its own dispatch for the end of the current frame of execution. Every resolver that runs synchronously up to its `load` call in that same frame contributes its key; when the call stack unwinds and the runtime turns to its deferred work, the dispatch runs and the batch function is called once with the accumulated keys. In an engine that resolves fields on a pool of threads or tasks, the executor itself owns the decision. It tracks how many fields it has started and how many are parked specifically on a loader's pending result. When those two numbers meet — every field it can reach is waiting on a loader and nothing else is runnable — it walks the request's loaders and dispatches all of them. The observable behaviour is the same either way, and that is what an interviewer is checking: the batch leaves at the *quiescence point*, not on a clock and not on the call to `load`. ## A worked example: the locker board Take a parcel-locker graph — hubs, lockers, compartments, parcels, couriers, routes. `Locker` is a fat type in this schema, 37 fields, and 12 of those fields are backed by loaders (the servicing route, the current courier, the last audit, the compartment roll-up, and so on). A board query asks for a page of 43 lockers and selects all 12 loader-backed fields. Execution reaches all 43 × 12 = 516 field resolutions in one pass, because none of them awaits anything before calling `load`. Every one queues a key and parks. Now nothing is runnable, so dispatch fires: **12 batch-function calls**, one per loader, each with at most 43 keys. Those 516 field values cost a dozen backend round trips. Then the results come back, the 516 parked fields resume, and some of them load again — the courier each locker resolved now needs its home hub, and those hub ids were not knowable until the couriers arrived. Those new keys queue and execution stalls again, so dispatch fires a second time. A document with three levels of derived keys gets roughly three rounds. **Dispatch happens once per stall, not once per request**, and the number of rounds tracks the document's dependency depth rather than its width. ## Why deferring the fetch is legal The GraphQL specification does not mention loaders, batching or dispatch, but it does grant the permission the technique needs. For a query selection set the fields' values may be resolved in any order, and an implementation is free to resolve them concurrently; the specification only requires that the **response** object's keys appear in the order the selection set requested them, which is about assembling the result map, not about when each value was computed. Only a mutation's root fields carry an ordering requirement — they execute serially. So a server may hand back a placeholder for `Locker.currentCourier`, go do something else, and fill the value in later without violating anything. ## Specified versus conventional Be explicit about this in an interview, because it is a cheap way to sound precise. The execution model above — deferred resolution, unordered field resolution for queries, serial mutation root fields — is **specification** text. The loader, its queue, its memo map and its dispatch trigger are **convention**: a design every ecosystem re-implemented from the same original, with real differences in the details. Whether dispatch re-arms after a batch completes, whether you can supply your own scheduler, and what happens to keys queued after the last stall are all implementation properties, and a candidate who says "it depends on the implementation, and here is what to check" is more convincing than one who recites a single library's behaviour as law. ## What interviewers listen for Three things. That `load` returns without fetching. That something later decides the queue is done, and that the something is *the absence of other runnable work*. And that this is why blocking a thread on a loader's pending result inside a resolver is dangerous: you may have blocked the very worker that was about to notice the stall and send your batch.
- What in the GraphQL specification makes it legal to defer a field's data fetch like this?For a query selection set the specification leaves field resolution order unspecified and permits an implementation to resolve fields concurrently; what it pins down is the order of keys in the response object, which is about assembling the result map rather than about when each value was computed. Only a mutation's root fields must execute serially. So returning a placeholder and filling it in after a batch arrives violates nothing.
- How many dispatch rounds should one request produce?As many as execution stalls with keys queued — roughly one per level of derived keys. A wide document that loads 516 keys in a single pass is one round. A document that loads a courier from a locker and then that courier's hub is two, because the second key is not knowable until the first result lands. Rounds scale with dependency depth, not with list width.
- Is a fixed timer ever the dispatch trigger?Some implementations let you supply your own scheduler, including a short delay, and that is occasionally used as an escape hatch. It is not what the pattern relies on: the default trigger is quiescence, and a delay-based dispatch pays its delay on every dependency level of every request. Treat a timer as a workaround you can name, not as the mechanism.
It is a shuttle bus rather than a taxi rank: calling load puts you in the queue, and the bus leaves when nobody else is walking towards it — not when you arrive, and not on a timetable.
saying these in an interview costs you the question
- Says the batch is sent the moment load is called
- Thinks a wall-clock timer is the dispatch trigger
- Assumes one request always produces exactly one batch
- Calls loader dispatch part of the GraphQL specification
- Blocks a thread on a pending result to force the batch out