Supabase Log Cleanup: Edge Functions and Cron Jobs

After working on several Supabase-backed projects, I kept hitting the same silent disk problem — log tables that grow forever. Here is the cleanup_logs() function and how to run it safely.

EP

Ender Puentes

Context: how I got here

Over the past few months I've worked on several projects whose backend runs on Supabase. In many of them, Edge Functions are a core piece — webhooks, async tasks, third-party integrations — and some also rely on pg_cron jobs. Everything worked fine until I started noticing a pattern: over time, the database kept approaching its disk limit even though business data volume didn't explain it.

The cause was log accumulation: execution records that Supabase writes directly into PostgreSQL and that are not automatically purged from the Dashboard.

Official Supabase documentation confirms:

  • Edge Functions: logs live in the edge_logs schema and in net._http_response.
  • Cron Jobs (pg_cron): each run appends rows to cron.job_run_details.

Without active maintenance, those tables grow indefinitely. In the worst case, the database enters read-only mode due to disk exhaustion — a severe service interruption.

This document records the problem, the cleanup_logs() function we implemented as a fix, and how to operate it safely in any Supabase project using Edge Functions or Cron Jobs.

The problem

  • cron.job_run_details · pg_cron — Run history: start_time, status, command, return_message, etc.
  • net._http_response · pg_net — HTTP responses from Edge Function invocations: created, status, headers, body, etc.

Every Edge Function call and every cron tick adds rows. There is no "clear logs" button in the Supabase UI. Growth is silent until disk runs out.

Observed consequences:

  • Progressive consumption of the project's allocated disk space.
  • Query performance degradation on those tables.
  • Read-only mode risk when disk hits the limit.

The solution: cleanup_logs()

A PL/pgSQL function with SECURITY DEFINER that deletes records older than a configurable threshold (default: 12 hours).

sql
CREATE OR REPLACE FUNCTION cleanup_logs()
RETURNS void
SET search_path = 'public'
AS $$
BEGIN
    -- Clean cron job logs
    DELETE FROM cron.job_run_details
    WHERE start_time < NOW() - INTERVAL '12 hours';

    -- Clean HTTP response logs (Edge Functions)
    DELETE FROM net._http_response
    WHERE created < NOW() - INTERVAL '12 hours';

    RAISE LOG 'Log cleanup completed at %', NOW();
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

COMMENT ON FUNCTION cleanup_logs() IS
  'Removes old cron job and Edge Function logs to prevent uncontrolled disk growth';

Technical characteristics

  • Security — SECURITY DEFINER — runs with owner privileges to access cron and net schemas
  • Scope — search_path = 'public' — avoids name resolution ambiguity
  • Retention — 12 hours by default; adjust by changing the interval in the function
  • Atomicity — Runs in a transaction; both DELETE statements are consistent
  • Observability — RAISE LOG leaves a trace in PostgreSQL logs

Step-by-step implementation

1. Migration

bash
npx supabase migration new create_cleanup_logs_function

Add the function SQL to the generated file and apply:

bash
# Local
npx supabase db reset

# Production
npx supabase db push

2. Maintenance cron job

From Supabase Dashboard → Database → Cron Jobs → Create Cron Job:

  • Name — cleanup-logs-maintenance
  • Schedule — */5 * * * * (every 5 minutes)
  • Command — SELECT cleanup_logs();
  • Active — ✅ Enabled

Note: The */5 * * * * schedule keeps tables bounded in high-traffic projects. For lighter loads, 0 */6 * * * (every 6 hours) may suffice.

3. Verification

sql
-- Scheduled job
SELECT jobname, schedule, active, command
FROM cron.job
WHERE jobname = 'cleanup-logs-maintenance';

-- Recent runs
SELECT start_time, status, return_message, command
FROM cron.job_run_details
WHERE command LIKE '%cleanup_logs%'
ORDER BY start_time DESC
LIMIT 10;

Monitoring

Diagnostic queries

sql
-- Current cron log size
SELECT
    COUNT(*) AS total_records,
    pg_size_pretty(pg_total_relation_size('cron.job_run_details')) AS table_size
FROM cron.job_run_details;

-- Current HTTP log size
SELECT
    COUNT(*) AS total_records,
    pg_size_pretty(pg_total_relation_size('net._http_response')) AS table_size
FROM net._http_response;

-- Temporal distribution (last 24 h)
SELECT
    DATE_TRUNC('hour', start_time) AS hour,
    COUNT(*) AS log_count
FROM cron.job_run_details
WHERE start_time > NOW() - INTERVAL '24 hours'
GROUP BY hour
ORDER BY hour DESC;

Optional threshold alert

sql
CREATE OR REPLACE FUNCTION check_log_size_alert()
RETURNS void AS $$
DECLARE
    cron_count INTEGER;
    http_count INTEGER;
BEGIN
    SELECT COUNT(*) INTO cron_count FROM cron.job_run_details;
    SELECT COUNT(*) INTO http_count FROM net._http_response;

    IF cron_count > 10000 OR http_count > 50000 THEN
        RAISE WARNING 'Log tables are growing too large: cron=%, http=%',
            cron_count, http_count;
    END IF;
END;
$$ LANGUAGE plpgsql;

Schedule check_log_size_alert() on a separate cron or call it from the same maintenance job if early visibility is needed.

Considerations

⚠️ Warnings

  • Irreversibility — Deleted logs cannot be recovered. Export first if needed for auditing.
  • Performance — Bulk DELETE may briefly impact the database. Tune retention and frequency to match load.
  • Dependencies — Requires pg_cron and pg_net extensions enabled (standard on Supabase).

✅ Best practices

  • Gradual retention: start with 24–48 h and reduce only if disk pressure demands it.
  • Low-traffic windows: on sensitive projects, schedule cleanup during quieter periods.
  • Proactive monitoring: review table sizes weekly until growth patterns stabilize.
  • Backups: consider periodic export of critical logs before purge if compliance requires it.

Quick reference

text
Function:   cleanup_logs()
Retention:  12 hours (configurable)
Cron:       cleanup-logs-maintenance → SELECT cleanup_logs();
Tables:     cron.job_run_details, net._http_response

More notes on Database Architecture.