At-least-once delivery
We commit to at-least-once delivery. That means:- If your endpoint responds 2xx, we mark the delivery successful and never re-send that event ID.
- If your endpoint times out, returns non-2xx, or we hit a transient infrastructure issue mid-delivery, we retry — and you may end up seeing the same event ID twice.
Idempotency on eventId
Every event carries a unique eventId in the envelope:
1
Verify the signature
HMAC-SHA256 check against the raw body. Reject any request that fails verification.
2
Check whether you've seen this eventId before
Look it up in your dedupe store (Postgres table, Redis SET, DynamoDB item — anything atomic with a unique constraint on the ID).
- Seen → return 200 immediately, skip processing.
- New → continue.
3
Insert the eventId BEFORE processing
Do an atomic insert-if-not-exists. If the insert fails because of a unique-constraint violation, treat as a duplicate (another worker is processing it, or it’s a retry that’s racing with the first attempt) and return 200.
4
Process the event in your business logic
Apply the change, update your records, fire side effects.
5
Return 200
A 2xx response signals “I have it; don’t send it again.” We mark the delivery durable at that point.
Minimal Postgres example
How long to retain dedupe entries? A month is plenty. Our retry window is well under 24 hours (see backoff below), so anything older than that won’t recur. Periodically purge old rows so the table doesn’t grow unbounded.
Retries and backoff
If your endpoint fails (non-2xx response, timeout > 10s, connection refused), the failed delivery is redelivered by Azure Service Bus under its default policy: exponential backoff in the seconds-to-tens-of-seconds range, up to a small handful of attempts (typically 5). After Service Bus exhausts retries, the message lands in a dead-letter queue and we stop trying for that specific event. Note this is per-event retry, not endpoint-level retry. A handful of retries over ~minutes — not a long-tail schedule that spans hours.Auto-pause on consecutive failures
What protects you from a sustained outage is a separate mechanism: three consecutive event-delivery failures in a row pauses the subscription. The counter increments on each failed event and resets to zero on any 2xx response.
When a subscription auto-pauses:
- The admin grid renders a Paused badge on the row (amber dot + “Paused” pill).
- FPT logs a Warning to elmah so internal monitoring catches it.
- In a near-term release, the gym owner will get an email notification with a link to investigate. (v1.1 ships the admin-visible state + log signal; SendGrid email wiring lands in v1.1.x.)
- An admin clicks the resume button (
▶) in the actions column to flip back to active.
Why not the long-tail retry schedule (1min, 5min, 30min, …)? In practice, partners who are down for an hour usually stay down for hours, and rapid auto-pause + visible admin signal beats invisible queueing. The 3-failure threshold is also the contract Close.com uses for the same reason.
What counts as success vs failure
Ordering
We don’t guarantee ordering across events. If a contact’s status flips Lead → Member → VIP within 200ms, you might receive the twocontact.status_changed events in either order. Plan for this:
- Sort by
eventTimestampwhen order matters - Coalesce at your end if you only care about the latest state (look up the contact’s current state at receipt time via your own data, not the event)
- Don’t make decisions from a single event’s
previousX → newXchain if multiple events of that type can fire close together — the previous values you see may be stale