Wake-Up Hints & Latency
Per-adapter latency: LISTEN/NOTIFY versus poll-interval fallback, why polling stays the sole source of truth, and graceful degradation.
A Worker finds work by polling. On a fixed cadence each Worker group asks its store for due Jobs on the Queues it serves, claims a batch, and runs them. That loop is the entire correctness story: if a Job is due and a Worker has headroom, polling will find it. A Wake-Up Hint is the optimization layered on top. It is a best-effort nudge that says "there may be new work on this Queue, poll it sooner," and its only job is to shave the gap between a Job becoming due and a Worker noticing. The hint can be dropped, duplicated, delayed, or reordered, and nothing breaks, because it never carries a correctness decision. Batched, bounded polling is the sole correctness mechanism; a Wake-Up Hint only ever brings a poll forward, never replaces it.
The rest of this page is detail on that: which adapters can send hints, how fast each one is, and what happens when a hint never arrives. That last answer never varies by adapter or failure mode — latency degrades to the poll interval, and nothing else does.
Hints are a latency optimization, never correctness#
The system behaves identically, minus latency, whether every hint fires perfectly or every hint is silently discarded. A hint for a Queue with no work is harmless: the Worker polls, finds nothing, and goes back to waiting. A hint that arrives twice is harmless: the second poll finds the Job already claimed. A hint that never arrives is harmless: the next ordinary poll picks the Job up. There is no code path anywhere that waits for a hint, counts hints, or treats their absence as meaningful.
This is why you must never design around them. Do not set a timeout, an SLA, or a "the Job ran instantly" assumption tighter than your poll interval. Worst-case pickup latency is always the poll interval, full stop. Hints make the common case feel immediate; they make no promise about the worst case. The correctness backstop underneath all of this is at-least-once delivery through polling and leasing, covered in Execution Guarantee.
The poll interval is the latency floor#
Every Worker group polls on a cadence you set, and that cadence is the number that bounds worst-case pickup latency. When hints are working, a Job is typically noticed almost as soon as it becomes due. When no hint arrives, or when an adapter has no hint channel at all, the Job waits at most one poll interval. There is one knob, and it is the same knob for every adapter:
b.AddWorkerGroup(new WorkerGroupOptions
{
Name = "default",
PollInterval = TimeSpan.FromMilliseconds(250), // tighter latency, more frequent store queries
// ...
});PollInterval is a TimeSpan on WorkerGroupOptions, and it defaults to one second. A shorter interval lowers latency at the cost of more frequent store queries; a longer one trades latency for fewer queries. It lives on the Worker group rather than on the store, which means the same setting governs fallback latency regardless of which adapter you run underneath. Lowering it is the universal latency lever, and for the one adapter that has no hint channel it is the only lever. Where PollInterval is configured in context is on the Worker Groups page, and its default sits alongside the others in Limits and Defaults.
The optional hint capability#
A Wake-Up Hint source is an optional part of the Storage Contract. An adapter that has a notification primitive implements it; an adapter that does not simply omits it, and latency falls back to the poll interval. The capability is one subscribe call: you give it a per-Queue callback and get back a handle you dispose to unsubscribe.
The callback receives the name of a Queue that may have newly eligible work. The contract is deliberately weak: the callback may be invoked concurrently, more than once for the same wake-up, or not at all, and you treat it purely as a "poll this Queue sooner" nudge, never as authoritative. A callback that never fires, or fires for a Queue that turns out to have no work, is always acceptable. The subscription self-heals: on channel loss it keeps reconnecting until it is disposed, and while it is down latency degrades to the poll interval and nothing else changes. The returned handle tears the subscription down when disposed and stops further callbacks.
You do not call this yourself. The Worker pump discovers the capability at runtime by checking whether the store implements it. If it does, the pump subscribes and lets hints accelerate its polls; if it does not, the timer alone drives polling. That runtime check is the graceful-degradation mechanism on the consumer side, and it is why dropping in an adapter without hints needs no configuration change. There are no public exceptions on the hint path by design: a hint failure never throws to your code, it is swallowed or retried internally.
How a hint accelerates a poll#
A hint can only ever speed up a poll the timer would eventually have done anyway, and even then it has to clear a few gates. It has to be for a Queue the group actually serves, or it is ignored. The group has to have pool headroom; when its in-flight pool is full the hint requests nothing, since the group never claims more than it can run. And because a hint and the periodic timer share a single pending-poll slot, a burst of hints while a poll is already queued collapses into one extra claim pass rather than a full cycle each. So a saturated group ignores hints until slots free up, and a flood of hints cannot stampede the store.
Per-adapter behavior#
The three production adapters sit at three different points on the latency spectrum, and the differences are worth knowing when you choose one.
| Adapter | Hint mechanism | Scope | Fires under your transactional enqueue? |
|---|---|---|---|
| Postgres | LISTEN/NOTIFY | Cross-process | Yes — on commit |
| SQLite | In-process notification | Single process | No — only on the store's own commit |
| SQL Server | None (polling only) | n/a | n/a (no hint channel) |
Postgres: LISTEN/NOTIFY#
Postgres implements the hint capability over its native LISTEN/NOTIFY mechanism on a dedicated notification channel. When the store enqueues a Job that is already due, it issues a notify carrying the Queue name; minting a due Job from a recurring schedule signals the same way. On the subscribe side it holds a dedicated connection listening on that channel and forwards each Queue name it receives to the callback.
Two properties make Postgres the strongest of the three. First, the notify is itself transactional, which has a consequence covered in the next section. Second, channel loss is treated strictly as a latency event, never a correctness one: the listener reconnects on its own, retrying every few seconds until the subscription is disposed, and while it is down polling carries everything at the poll interval. Postgres needs no configuration to enable any of this. The listener targets the same database as the store, and the whole mechanism is automatic. See the Postgres adapter page for the broader picture.
SQLite: in-process notification#
SQLite implements the hint capability with an in-process notification hub. After the store commits new due work, it nudges the Worker pumps running in the same process to poll sooner. This is honest and important: SQLite hints are in-process only. A SQLite database file can be shared by several processes on one host, but a commit in one process does not hint pumps in another. Pumps in other processes fall back to the poll interval. Correctness is untouched, because polling still picks the Job up; only latency differs across the process boundary.
A burst of commits for the same Queue coalesces into a single delivered hint, and a slow or throwing callback in one subscriber cannot block publishing or starve another subscriber. You can turn the mechanism off through the one adapter-level knob related to hints:
new SqliteStoreOptions
{
ConnectionString = "Data Source=app.db",
EnableInProcessHints = false, // default is true
}EnableInProcessHints is a bool defaulting to true. When it is false, the store hands the pump a no-op subscription and polling carries everything at the poll interval. You rarely need to touch it. More on the adapter is on the SQLite page.
SQL Server: polling only#
The SQL Server adapter does not implement the hint capability. There is no Service Broker or query-notification integration and no hint option on its store options. SQL Server runs entirely on the poll loop, so pickup latency is bounded by PollInterval (one second by default). This is not a degraded mode you fell into; it is the steady state for the adapter, and it is fully correct because polling is the sole correctness mechanism in the first place. If you want lower latency on SQL Server, lower PollInterval. There is no channel to switch on. See the SQL Server adapter page for details.
Transactional enqueue and the hint#
When you enqueue a Job inside your own transaction, the timing of the hint depends on the adapter, and only on the adapter. ADO.NET exposes no commit callback, so an adapter that fired its hint before your commit would wake the pump to a row it cannot yet see. Two of the three adapters handle this by simply not firing on the transactional path.
- Postgres is the exception. Its notify rides the same commit as the Job row, so the hint fires on your commit and is never visible before the row is. You get atomicity and low latency together.
- SQLite holds the hint back. It nudges only after the store's own commit, never on a transactional enqueue you commit yourself. The Job is picked up on the next ordinary poll.
- SQL Server has no hint channel at all, so the question is moot. The Job is picked up on the next poll regardless.
Either way this is latency, not correctness. The Wake-Up Hint is never correctness-bearing; polling picks the Job up regardless. The practical takeaway: with Postgres plus transactional enqueue you get both atomicity and near-immediate pickup; with SQLite or SQL Server plus transactional enqueue you get atomicity, and the Job is claimed within at most one poll interval.
Sharp edges worth remembering#
- A hint is never a guarantee. Worst-case latency is always the poll interval. Do not build timeouts or SLAs tighter than it.
- SQL Server has no hints. Latency is purely the poll interval; lower
PollIntervalto speed pickup, because there is nothing to enable. - SQLite hints are in-process only. Multi-process deployments on one host get hints within each process; cross-process pumps fall back to polling.
- Transactional enqueue suppresses the hint on SQLite, and SQL Server has none. Only Postgres hints through commit. Expect up-to-one-poll-interval latency for transactionally enqueued Jobs on SQLite and SQL Server.
- Hints only help Queues a group serves, and only with pool headroom. A saturated group ignores hints until slots free up.
- Duplicate and spurious hints are normal and harmless. A hint for an empty Queue just triggers a no-op poll.
Where to go next#
- Execution Guarantee: why polling and leasing, not hints, carry at-least-once delivery.
- Worker Groups: where
PollIntervalis configured. - Transactional Enqueue with EF Core: the latency-versus-correctness note in full context.
- Choosing an Adapter: the latency tradeoff across adapters.
- Postgres, SQLite, and SQL Server: each adapter's hint behavior in place.
- Storage Contract: where the optional hint capability is specified.
- Limits and Defaults: the
PollIntervaldefault and the rest.