Skip to content

Feature: configurable time-based retention for events (and cascaded attempts) #354

Description

@wrightbuilt

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:

  1. Batching. Should the purge delete in batches to avoid long-held locks on large tables, rather
    than issuing one unbounded DELETE?
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions