Hook Processors
Run durable hooks and background work for database-backed fragments.
Overview
Database-backed fragments can schedule durable hooks. Durable hooks are stored in the database as part of the same transaction as your mutations, then executed after the commit with retries.
A hook processor is the background process that wakes up and processes these hooks, so retries and scheduled work happen even when no new requests arrive. In code, the runtime helpers create a dispatcher directly from one or more fragments, and internally build the durable hooks processor they drive.
When you need a hook processor
You need a hook processor when a fragment uses durable hooks.
If a fragment does not define durable hooks, createDurableHooksProcessor([fragment]) throws with
details about the missing configuration.
How it works (from the code)
Durable hook processing is designed for at-least-once delivery and idempotent handlers:
- Hook triggers are recorded in the same transaction as your mutations.
- A processor claims pending events in the DB to avoid double-processing.
- Each event is executed outside the transaction, then marked
completedorfailed. - Failures are retried with exponential backoff.
- Events stuck in
processingtoo long are re-queued (default 10 minutes).
Dispatchers use getNextWakeAt() to decide when work is due. If there is nothing scheduled, they
sleep. If work is due, they run processor.process(). The hook processor is the full loop: a
dispatcher plus the processor it drives.
Durable hooks are at-least-once. Your hook handlers must be idempotent.
Node hook processor (polling)
Use the Node hook processor for long-lived servers or local development:
import { createDurableHooksProcessor } from "@fragno-dev/db/dispatchers/node";
const dispatcher = createDurableHooksProcessor([fragment], {
pollIntervalMs: 2000,
});
if (dispatcher) {
dispatcher.startPolling();
process.on("SIGTERM", () => dispatcher.stopPolling());
}Notes:
pollIntervalMsdefaults to 5000.- Calling
dispatcher.wake()triggers immediate processing. - The dispatcher serializes runs internally, so concurrent calls are queued.
Cloudflare Durable Objects hook processor (alarms)
Use the Durable Object dispatcher to schedule processing via alarms. The hook processor should be created in the same DO instance that owns the fragment:
import { DurableObject } from "cloudflare:workers";
import { createDurableHooksProcessor } from "@fragno-dev/db/dispatchers/cloudflare-do";
import { migrate } from "@fragno-dev/db";
import { createMyFragmentServer, type MyFragment } from "@/fragno/my-fragment";
export class MyFragmentDO extends DurableObject<Env> {
fragment: MyFragment;
handler: ReturnType<ReturnType<NonNullable<typeof createDurableHooksProcessor>>>;
constructor(state: DurableObjectState, env: Env) {
super(state, env);
this.fragment = createMyFragmentServer({ env, state, type: "live" });
state.blockConcurrencyWhile(async () => {
await migrate(this.fragment);
});
// Keep the dispatcher in the same DO instance that owns the fragment.
const dispatcher = createDurableHooksProcessor([this.fragment]);
this.handler = dispatcher(state, env);
}
alarm() {
return this.handler.alarm?.();
}
}Notes:
- The dispatcher schedules the next alarm based on
getNextWakeAt(). - If no work is pending, it clears the alarm.
state.storage.setAlarmis required.- For Durable Objects, keep the dispatcher in the same DO instance that owns the fragment so it uses the same storage and scheduling lifecycle.
Configuration knobs
You can tune durable hook behavior when instantiating the fragment:
const fragment = instantiate(fragmentDef)
.withConfig(config)
.withOptions({
databaseAdapter,
durableHooks: {
stuckProcessingTimeoutMinutes: 10,
onStuckProcessingHooks: ({ namespace, timeoutMinutes, events }) => {
console.warn("Re-queued stuck hooks", namespace, timeoutMinutes, events.length);
},
},
})
.build();Operational guidance
- Run one hook processor per deployment process. Multiple processors are safe but may add load.
- Ensure your hook handlers are idempotent.
- For serverless platforms, use a dispatcher that integrates with the platform scheduler (e.g., Cloudflare Durable Objects).
Troubleshooting
- Hooks never run: verify the dispatcher is started and
createDurableHooksProcessoris called. - Work is delayed: check
pollIntervalMsor alarm scheduling. - Repeated retries: check hook error logs and implement idempotency.
- Stuck hooks: confirm
stuckProcessingTimeoutMinutesis set and logs are visible.