Blog

When a calendar cannot be read at all

icalwebhookslimits

Some failures are worth waiting out. A provider that times out at 3am is usually fine by 3:05, and an integration that treats every blip as a disaster spends its life crying wolf.

Some failures are not like that. A calendar that is too large to read will be too large to read on the next attempt, and the attempt after that. The condition is permanent in the strict sense: the same request returns the same bytes.

The limit

We read calendars up to 8 MiB and no further. That number is far beyond any real export — a calendar with every booking for a decade is a few hundred kilobytes — so a file over it is almost always a symptom rather than a calendar: an export that concatenated several properties, or a URL that returns a report instead of a feed.

The important part is what it is not: it is not something a retry can clear. Waiting cannot help, and telling a customer to wait is worse than telling them nothing.

Three events, not one event with a flag

A webhook that says "something went wrong" and leaves the reader to guess is the root of most integration bugs. We send three different events, because they call for three different responses:

Event Says What you should do
ical.changed A calendar moved Update your copy
ical.failure_rate Our polling is failing more than your thresholds allow Watch; it may recover
ical.error This subscription cannot work at all Stop waiting and act

The split matters because the actions are opposites. An alert is a request for patience; an error is a request for work. Collapsing them into one event with a severity field forces every consumer to branch on that field to find out which conversation it is in.

The payload

ical.error carries no calendar content — there is nothing to diff — and no changes block, because three empty buckets would read as "nothing happened", which is the opposite of the message:

{
  "event": "ical.error",
  "subscription_id": "3f9c…",
  "external_id": "villa-42",
  "timestamp": "2026-10-12T08:15:00Z",
  "error": {
    "code": "calendar_too_large",
    "message": "the calendar is larger than our limit of 8 MiB, so it cannot be read at all; no retry will change this until the calendar is smaller",
    "permanent": true,
    "feed_id": "a18deb5c-1666-4503-98a8-c290204a66c7",
    "provider": "Smoobu",
    "limit_bytes": 8388608,
    "detected_at": "2026-10-12T08:15:00Z"
  }
}

permanent: true is the load-bearing field, and it is a promise: we only set it when we can prove that retrying cannot help. code is the field to key on — the message is for the human reading the alert, and it names the limit so nobody has to file a ticket to find out what it is.

Sent once, on the first poll

The condition stays true on every poll, so repeating the event would turn one oversized calendar into an endless stream. We record that we have told you and stay quiet until the calendar becomes readable.

Only the first poll, though. A feed that was readable yesterday and is oversized today is a different story: the provider changed the calendar and may change it back, so that is a stale integration rather than a broken one, and it shows up in the feed's health and request log rather than in a webhook that claims the situation is final.

The full contract is in the API reference.

All posts