Summary
WebhookX persists every event, attempt, and attempt detail to Postgres, but there is currently no
way to bound how long that data lives. I'd like to propose configurable time-based retention as
a small, first-class operational control.
This isn't urgent for us — we have a workaround — but it feels like the kind of thing most
self-hosted deployments will eventually need, and it seems cheap to add given the machinery already
in the codebase.
What we're seeing
A single-source deployment (GitHub webhooks -> one endpoint), running since late May:
| table |
size |
rows |
attempt_details |
66 MB |
14,944 |
events |
65 MB |
14,899 |
attempts |
8.2 MB |
14,944 |
~139 MB over 63 days, roughly 236 events/day and ~2.2 MB/day, from one low-volume source.
attempt_details is the largest consumer rather than the events themselves. That extrapolates to
somewhere near a gigabyte a year, growing without bound, with no built-in way to trim it.
What I checked first
I wanted to be sure I wasn't missing an existing knob (on v1.1.0, and re-checked against main):
- No retention/TTL/expiry option in
config/config.go or any of the modules under config/modules/.
services/schedule exists and tasks are registered, but the set is app.plugin_rebuild,
anonymous_reports, license.expiration, worker.requeue, and worker.detectEndpointHealthy —
nothing that prunes.
admin/api/events.go implements PageEvent, GetEvent, CreateEvent, and RetryEvent — there's
no delete handler (DELETE /workspaces/{ws}/events returns 405).
If any of that is wrong or there's an intended path I've missed, I'd genuinely welcome the
correction.
That third point is really why I'm filing rather than just scripting around it: with no delete on
the Admin API, there is no supported path to bound this data from outside the application — the
only option available to an operator today is reaching into the schema directly. A first-class
retention setting is the difference between a supported control and every deployment inventing its
own.
Proposal (deliberately narrow)
Time-based purge only, as a first cut:
retention:
enabled: true
events: 30d # delete events older than this
interval: 1h # how often the purge runs
A single retention task registered on the existing Scheduler (alongside worker.requeue) seems
like a natural fit — and the interval could equally well be a fixed internal default now and become
configurable later, if you'd rather keep the surface smaller.
One nice property: because attempts.event_id -> events and attempt_details.id -> attempts are
both ON DELETE CASCADE, deleting from events reclaims all three tables in one statement — so the
implementation may be smaller than the feature sounds.
Deliberately out of scope for this request, to keep it tractable: per-source or per-workspace
windows, size-based caps, archival/export before deletion, and separate retention for successful vs
failed attempts. Those are all reasonable, but a single global time window would cover the common
case and could be extended later without a breaking change.
Open questions
Two things I don't have a strong opinion on, flagging them since they'd shape the implementation:
- Batching. Should the purge delete in batches to avoid long-held locks on large tables, rather
than issuing one unbounded DELETE?
- First run on an existing deployment. The initial prune could free a large fraction of the
table. Is that worth a note in the docs about post-delete bloat and a one-time VACUUM FULL, or
is leaving it to autovacuum fine in practice?
Meanwhile
We're planning a scheduled DELETE FROM events WHERE created_at < now() - interval '<N> days'
against the database directly. It works — but per the Admin API point above, it's the only option,
and it couples our operational tooling to your schema. That coupling is what we'd rather not depend
on long-term.
Offer
Happy to attempt a PR if you'd welcome one and are comfortable with the shape above. I should be
upfront that it would likely be a while before I could get to it, so please don't treat this as
claimed work — if someone else picks it up sooner, that's a better outcome. Equally happy to test a
branch against a real deployment with the data volumes above if that's more useful.
Thanks for WebhookX — the declarative config and the function plugin have made this a genuinely
pleasant gateway to operate.
Summary
WebhookX persists every event, attempt, and attempt detail to Postgres, but there is currently no
way to bound how long that data lives. I'd like to propose configurable time-based retention as
a small, first-class operational control.
This isn't urgent for us — we have a workaround — but it feels like the kind of thing most
self-hosted deployments will eventually need, and it seems cheap to add given the machinery already
in the codebase.
What we're seeing
A single-source deployment (GitHub webhooks -> one endpoint), running since late May:
attempt_detailseventsattempts~139 MB over 63 days, roughly 236 events/day and ~2.2 MB/day, from one low-volume source.
attempt_detailsis the largest consumer rather than the events themselves. That extrapolates tosomewhere near a gigabyte a year, growing without bound, with no built-in way to trim it.
What I checked first
I wanted to be sure I wasn't missing an existing knob (on v1.1.0, and re-checked against
main):config/config.goor any of the modules underconfig/modules/.services/scheduleexists and tasks are registered, but the set isapp.plugin_rebuild,anonymous_reports,license.expiration,worker.requeue, andworker.detectEndpointHealthy—nothing that prunes.
admin/api/events.goimplementsPageEvent,GetEvent,CreateEvent, andRetryEvent— there'sno delete handler (
DELETE /workspaces/{ws}/eventsreturns 405).If any of that is wrong or there's an intended path I've missed, I'd genuinely welcome the
correction.
That third point is really why I'm filing rather than just scripting around it: with no delete on
the Admin API, there is no supported path to bound this data from outside the application — the
only option available to an operator today is reaching into the schema directly. A first-class
retention setting is the difference between a supported control and every deployment inventing its
own.
Proposal (deliberately narrow)
Time-based purge only, as a first cut:
A single retention task registered on the existing
Scheduler(alongsideworker.requeue) seemslike a natural fit — and the interval could equally well be a fixed internal default now and become
configurable later, if you'd rather keep the surface smaller.
One nice property: because
attempts.event_id -> eventsandattempt_details.id -> attemptsareboth
ON DELETE CASCADE, deleting fromeventsreclaims all three tables in one statement — so theimplementation may be smaller than the feature sounds.
Deliberately out of scope for this request, to keep it tractable: per-source or per-workspace
windows, size-based caps, archival/export before deletion, and separate retention for successful vs
failed attempts. Those are all reasonable, but a single global time window would cover the common
case and could be extended later without a breaking change.
Open questions
Two things I don't have a strong opinion on, flagging them since they'd shape the implementation:
than issuing one unbounded
DELETE?table. Is that worth a note in the docs about post-delete bloat and a one-time
VACUUM FULL, oris leaving it to autovacuum fine in practice?
Meanwhile
We're planning a scheduled
DELETE FROM events WHERE created_at < now() - interval '<N> days'against the database directly. It works — but per the Admin API point above, it's the only option,
and it couples our operational tooling to your schema. That coupling is what we'd rather not depend
on long-term.
Offer
Happy to attempt a PR if you'd welcome one and are comfortable with the shape above. I should be
upfront that it would likely be a while before I could get to it, so please don't treat this as
claimed work — if someone else picks it up sooner, that's a better outcome. Equally happy to test a
branch against a real deployment with the data volumes above if that's more useful.
Thanks for WebhookX — the declarative config and the
functionplugin have made this a genuinelypleasant gateway to operate.