Skip to main content

max / makenotwork

Prune processed-webhook dedup markers (bounded growth) processed_webhook_events gained one row per Stripe event and was never pruned — the daily scheduler prunes its siblings but not this table, so it grew for the life of the deployment (Run #21 Performance SERIOUS). Add prune_processed_events and run it daily at 30 days, the retention the table was created with (migration 065); Stripe won't redeliver events that old, so the dedup guarantee is unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-06-15 23:10 UTC
Commit: deae4c6bf43cc8da41a83888dd4d6596ff6e71e7
Parent: 7652c93
2 files changed, +29 insertions, -0 deletions
@@ -57,6 +57,24 @@ pub async fn mark_event_processed(pool: &PgPool, event_id: &str) -> Result<()> {
57 57 Ok(())
58 58 }
59 59
60 + /// Delete processed-event dedup markers older than `days`. These markers only
61 + /// guard against Stripe *redelivering* an event, which it stops doing within a
62 + /// few days; 30 days is the retention the table was created with (migration
63 + /// 065) but never enforced — without this prune the table grows one row per
64 + /// webhook for the life of the deployment (Run #21 Performance SERIOUS).
65 + #[tracing::instrument(skip_all)]
66 + pub async fn prune_processed_events(pool: &PgPool, days: i64) -> Result<u64> {
67 + let result = sqlx::query(
68 + "DELETE FROM processed_webhook_events \
69 + WHERE processed_at < NOW() - make_interval(days => $1::int)",
70 + )
71 + .bind(days as i32)
72 + .execute(pool)
73 + .await?;
74 +
75 + Ok(result.rows_affected())
76 + }
77 +
60 78 /// Insert a failed webhook event for later retry.
61 79 #[tracing::instrument(skip_all)]
62 80 pub async fn insert_failed_event(
@@ -286,6 +286,17 @@ pub fn spawn_scheduler(
286 286 }
287 287 Err(e) => tracing::error!(error = ?e, "failed to prune page views"),
288 288 }
289 +
290 + // Prune processed-webhook dedup markers older than 30 days. Stripe
291 + // won't redeliver events that old, so they no longer prevent a
292 + // duplicate; the table is otherwise append-only and grows one row
293 + // per webhook forever (Run #21 Performance SERIOUS).
294 + match db::webhook_events::prune_processed_events(&state.db, 30).await {
295 + Ok(n) => {
296 + if n > 0 { tracing::info!(pruned = n, "pruned old processed-webhook markers"); }
297 + }
298 + Err(e) => tracing::error!(error = ?e, "failed to prune processed-webhook markers"),
299 + }
289 300 }
290 301
291 302 // Explicitly release the advisory lock (defense-in-depth: also released