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.
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).
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
npx supabase migration new create_cleanup_logs_functionAdd the function SQL to the generated file and apply:
# Local
npx supabase db reset
# Production
npx supabase db push2. 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
-- 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
-- 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
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
Function: cleanup_logs()
Retention: 12 hours (configurable)
Cron: cleanup-logs-maintenance → SELECT cleanup_logs();
Tables: cron.job_run_details, net._http_response