Farbeam
Docs / Alerts and webhooks

Webhook retries

A delivery gets up to four attempts. After the fourth failure it is marked failed. There is no later replay, and no endpoint to resend a delivery.

#Schedule

AttemptSent after the prior failureEarliest time after the match
1At once0 to 3 s
230 sAbout 30 s
35 minAbout 5.5 min
430 minAbout 35.5 min

A worker runs every 3 seconds and handles up to 20 due deliveries per pass, in id order. In busy periods, an attempt can start later than the table shows. Each attempt has a timeout of 6 seconds.

The delay is fixed and has no jitter.

#What counts as failure

OutcomeLog errorLog code
Status 2xxNone. This is delivered.The status
Status 3xx, 4xx or 5xxnon-2xx replyThe status
No response in 6 stimeoutnull
DNS failure, refused connection, TLS failureconnection failednull
Host resolves to a private addresshost resolves to a private addressnull
The URL fails the rules at send timeThe URL rule messagenull

All of these are retried, including a 4xx. There is no way for your endpoint to say "stop". Use 2xx to accept an event even when you choose to ignore it. A 401 from your signature check is retried too, which helps if your deployment was briefly misconfigured.

Your response body is not stored or read.

#What stays the same across attempts

ItemAcross attempts
Body bytesSame. sent_at does not change.
t in the signatureNew on each attempt
v1 in the signatureNew, because t changed
x-farbeam-ruleSame
URLThe rule's current URL at each attempt. If you PATCH the URL, pending retries go to the new one.
SecretThe rule's secret. It cannot change.

#Reading the state of a delivery

curl -s "https://farbeamlabs.com/api/v1/rules/$RULE_ID/deliveries?limit=10" \
  -H "Authorization: Bearer $FARBEAM_KEY"
const res = await fetch(`https://farbeamlabs.com/api/v1/rules/${process.env.RULE_ID}/deliveries?limit=10`, {
  headers: { Authorization: `Bearer ${process.env.FARBEAM_KEY}` },
});
for (const d of (await res.json()).data) {
  console.log(d.ts, d.status, d.attempts, d.code, d.error);
}
import os, requests

res = requests.get(
    f"https://farbeamlabs.com/api/v1/rules/{os.environ['RULE_ID']}/deliveries",
    params={"limit": 10},
    headers={"Authorization": f"Bearer {os.environ['FARBEAM_KEY']}"},
    timeout=30,
)
for d in res.json()["data"]:
    print(d["ts"], d["status"], d["attempts"], d["code"], d["error"])
statusattemptsMeaning
pending0Queued, not tried yet
pending1 to 3Failed so far, and a retry is scheduled. error holds the last failure.
delivered1 to 4Done. error is null.
failed4All attempts failed

The log does not show the next attempt time, and it has no per-attempt history. It holds the last result.

#Interaction with limits

LimitInteraction
Hourly cap of 120Counted when a match is queued. Retries do not add to it.
Auto-off at 20Counts deliveries that ended as failed, not attempts. A delivery that succeeds on attempt 3 resets the count to 0.
Rule turned offPending retries still run. The off state only stops new matches.
Rule deletedPending retries are removed with it.
Service restartPending deliveries are stored in the database, so they survive a restart and continue.

The time to auto-off depends on your match rate. At 20 matches or more in a short burst, a dead endpoint can reach the limit about 36 minutes after the first failure, once all 20 have run through their attempts.

#Designing for failure

  • Answer fast, then process. Most timeouts come from doing work inside the handler.
  • Make handling idempotent. Key on rule_id and data.uid.
  • Monitor consecutive_failures from List rules, and alert yourself above a few. Farbeam sends no notice when a rule turns off.
  • If your endpoint will be down for more than 36 minutes, expect lost deliveries. To recover, read List large transfers for that period. Your rule's logic can be repeated with Validate a rule for the newest 2,000 rows.

#Recovering a gap

JavaScript
// Fetch rows in a period and apply your own filter, after an outage.
const since = Date.parse("2026-11-04T18:00:00Z");
const rows = [];
let before = null;
while (true) {
  const query = new URLSearchParams({ min_usd: "1000000", limit: "100" });
  if (before) query.set("before", String(before));
  const res = await fetch(`https://farbeamlabs.com/api/v1/transfers?${query}`);
  const { data, meta } = await res.json();
  rows.push(...data.filter((row) => Date.parse(row.ts) >= since));
  const oldest = data[data.length - 1];
  if (!oldest || Date.parse(oldest.ts) < since || meta.next_cursor === null) break;
  before = meta.next_cursor;
}
console.log(rows.length);

Rows stay available for 7 days. Because ts can be out of step with insert order by a few seconds, widen since a little.