Part 3 of 7 · Review request sender series ~7 min read

How the ask gets timed

Timing is most of the craft here. Ask the moment the job ends and you sound like you’re fishing; ask a week later and the goodwill has cooled; ask at 2am and you look broken. This post is about the clock: how each request gets its own one-off schedule at the right delay, kept inside opening hours, and re-checked the instant before it fires.

Key takeaways

  • The ask never goes out the instant a job ends — that reads as grabby. Each request waits a sensible delay set per business.
  • Each request gets its own one-off EventBridge Scheduler schedule at the computed due time — a timer per ask, not a busy polling loop.
  • The due time is snapped inside opening hours, so a job finished at 8pm becomes an ask the next morning, never a buzz at 2am.
  • At fire time the request is re-checked: opted out since scheduling? already asked? still eligible? If quiet hours have crept in, it reschedules.
  • The clock is entirely deterministic. The model is never asked when to send — only, later, how to grade and how to phrase.

The window is narrow

Timing is most of the craft in a review request, and it’s the part cheap bulk tools get wrong. Ask the moment the job ends and you sound like you’re fishing before the customer has even got home; the valet’s still drying, the physio session’s barely over, the boiler’s not been through a cold evening yet. Ask a week later and the warmth has cooled to indifference. Ask at 2am because that’s when the job happened to close in the system, and you look broken. The good window is a day or so out, when the work has proved itself and the customer still feels the benefit — and always at an hour a real business would actually message someone.

So timing gets its own stage, and one firm rule: the delay and the hour are computed by plain code, never by a model. The catch function in Part 2 handed over a request; this step decides when it fires and makes sure that when it does, the world hasn’t changed underneath it.

A timer per ask

Each request gets its own one-off schedule in EventBridge Scheduler — a managed service built for exactly this: enormous numbers of one-time schedules, each firing a Lambda at a specific moment, with no always-on compute and effectively no cost. When a request is created, the code computes a due time — the configured delay (say 24 hours) added to the completion, then snapped forward into the next opening-hours window — and creates a schedule with an at() expression for that exact instant, named deterministically from the request id. Because the name is derived from the request, creating it twice is a harmless no-op, which keeps the whole thing idempotent even if Part 2 retries.

This is the neat inversion of the usual advice. A periodic “sweep every 15 minutes and see what’s due” loop works, but it wakes up constantly to mostly find nothing, and it fires a batch of asks all at the same tick. A schedule per request fires each ask at its own precise, business-appropriate moment and sleeps the rest of the time — and modern EventBridge Scheduler makes one-off schedules cheap enough that the old “don’t create a timer per item” worry no longer applies. A small periodic sweep still exists, but only as a safety net for schedules that misfire (Part 7), not as the main clock.

Timing the ask: compute a due time, snap into opening hours, one-off schedule, then re-check at fire time before sending On the left, a box “Request created” from Part 2 sends an arrow into a box “Compute due time” inside a dotted AWS account container. Compute due time reads a settings box holding the delay and opening hours, adds the delay to the completion time, and snaps the result forward into the next opening-hours window. An arrow goes to a box “EventBridge Scheduler” labelled one-off schedule, at(due), named from the request id, which writes the due time onto the DynamoDB requests table. Time passes, shown by a dashed arrow labelled waits, until the schedule fires into a box “Sender wakes”. From there a decision box “Re-check: opted out? already asked? in quiet hours?” branches three ways: opted out or already asked exits to a “Drop, close request” box; in quiet hours exits to a “Reschedule to next open slot” box that loops back to the scheduler; otherwise a “proceed” arrow goes to a box “Grade & send” handed to Parts 4 and 5. A note reads: the delay and hour are computed by code and re-checked at fire time; the model is never asked when to send. Request created from Part 2 AWS account Compute due time delay + opening hours Settings delay, hours EventBridge one-off at(due), by id DynamoDB requests due time, state Sender wakes schedule fires waits ~1 day Re-check opt-out, once, quiet Drop, close request opted out / asked Reschedule next open slot quiet Grade & send Parts 4 & 5 proceed The delay and hour are computed by code and re-checked the instant before sending — the model is never asked when.
Fig 3. Timing the ask. A due time is computed from the delay and opening hours, a one-off EventBridge schedule is created for that instant and named from the request id, and when it fires the sender re-checks opt-out, once-only, and quiet hours — dropping, rescheduling, or proceeding to grade and send.

Snapped inside opening hours

Opening hours are what turn a raw “completion time plus 24 hours” into a moment a human would actually pick. The settings doc holds the business’s hours and a preferred send window — a valeter might like late morning, a physio clinic early evening after people leave work. The code adds the delay, then snaps the result forward to the next point inside that window: a boiler service finished at 8pm on Friday doesn’t become an 8pm-Saturday ask, it becomes a Saturday- or Monday-morning one depending on whether the business works weekends. Nobody gets a review request at an hour that says “this came from a machine that doesn’t sleep”.

Because a request can sit on the clock for a day or more, the world can change while it waits — which is why fire time is a checkpoint, not a blind send. The instant the schedule fires, the sender re-reads the request and re-runs the guards from Part 2: has the customer opted out since we scheduled this? Have they somehow already been asked through another route? Is this request still eligible? And crucially, has the due time drifted into quiet hours because of a bank holiday or a settings change — in which case it’s pushed to the next open slot rather than sent. Only a request that clears all of that proceeds to be graded and written. Everything else is dropped and closed, or quietly moved. The clock is precise, but it always defers to the guardrails.

Design rules that shaped the timing

  • Never the instant it ends. A configured delay lets the work prove itself before the ask — grabby is worse than late.
  • A schedule per ask. One-off EventBridge schedules fire each request at its own precise moment, with no always-on polling.
  • Snap into opening hours. The due time is moved to a sensible send window, so no ask ever lands at 2am.
  • Fire time is a checkpoint. Opt-out, once-only, and eligibility are re-checked the instant before sending, not just at scheduling.
  • Quiet hours win. If the moment has drifted into a closed window, the request reschedules rather than sends.
  • Code owns the clock. Every timing decision is deterministic; the model is only ever asked to grade and to phrase, later.
All posts