Back to blog
André Gacitua

André Gacitua @gacitua.dev

Project experience

16 GB Disk Size with Only 150 Users (and How I Solved It)

Supabase auto-scaled the Sideout disk from 18 GB to 27 GB. The app data was fine — a recursive webhook loop had silently ballooned two internal extension tables to 16 GB.

Got an automated email from Supabase: the Sideout project disk had been auto-scaled from 18 GB to 27 GB. The app had about 150 users, 25 events, and a handful of posts. That size made no sense.

First look: where is the data?

A quick query to check the actual database size confirmed it: 16 GB. Then I ran a table-size breakdown across all schemas — not just public — and the picture became clear immediately:

net._http_response          14 GB
supabase_functions.hooks    1713 MB
cron.job_run_details        51 MB
public.profiles             10080 kB
public.notifications        768 kB

The entire public schema was a rounding error. The weight was in internal Supabase extension tables: net._http_response (used by the pg_net extension for async HTTP calls) and supabase_functions.hooks (webhook audit history).

The culprit: a recursive webhook

Digging into supabase_functions.hooks, I found over 13 million rows — all from a single webhook named generate-profile-embedding. The app had 1,962 notification rows total. Something was looping.

The trigger was configured to fire on every UPDATE on public.profiles, with no conditions. The flow:

  1. A profile is updated.
  2. The webhook fires and calls an Edge Function.
  3. The Edge Function generates a vector embedding and writes it back to public.profiles.
  4. That write triggers another UPDATE.
  5. Go to step 2.

The sync-subscriber webhook was also listening to all profile updates, so it was caught in the same loop. The feature itself — semantic player matching — was fine. The trigger was the problem.

Cleanup

Truncating the two bloated tables brought the database from 16 GB down to about 82 MB:

truncate table net._http_response;
truncate table supabase_functions.hooks;

That’s the fastest part. The fix that actually mattered was tightening the trigger.

The fix: scoped triggers + defensive function

On the trigger side, I dropped the broad UPDATE trigger and replaced it with two precise ones:

  • An INSERT trigger for new profiles.
  • An UPDATE OF full_name, city, state, experience_level, bio trigger with an explicit WHEN clause that checks if any of those fields actually changed.
create trigger "generate-profile-embedding-update"
after update of full_name, city, state, experience_level, bio on public.profiles
for each row
when (
  old.full_name is distinct from new.full_name or
  old.city is distinct from new.city or
  old.state is distinct from new.state or
  old.experience_level is distinct from new.experience_level or
  old.bio is distinct from new.bio
)
execute function supabase_functions.http_request(...);

On the Edge Function side, I added a guard that compares record and old_record from the webhook payload. If none of the embedding fields changed, it returns early with { "skipped": true }. Two layers of protection, in case the broad trigger ever comes back by accident.

There was also a deploy issue — a 403 from an outdated Supabase CLI. Fixed by upgrading from 2.90.0 to 2.98.2 and using a personal access token instead of browser login.

What I’d do differently

Any webhook that writes back to the same table it listens to is a loop waiting to happen. The rule now: always use UPDATE OF specific_columns plus a WHEN clause. And treat supabase_functions.hooks and net._http_response as tables worth monitoring — they don’t show up in the usual app dashboards, but they can quietly dominate your disk.

#supabase#postgresql#sideout#backend#debugging

Project context

If this post was useful to you, hit the button below:

Discussion

Comments

Read the conversation or sign in with Google to add your own note.

0 comments

Want to comment?

Sign in with Google only when you want to write. Reading comments stays public.

More from the blog

André Gacitua

André Gacitua @gacitua.dev

Opinion

Ainda vale a pena usar WordPress em 2026?

WordPress é a resposta padrão para quem quer um site, mas na prática a maioria das pessoas acaba tão dependente de um dev quanto qualquer outra solução.

#wordpress#produto#web#opinião
André Gacitua

André Gacitua @gacitua.dev

Short note

Bem-vindo ao blog do gacitua.dev

Um espaço para registrar experiências de desenvolvimento, decisões de produto e reflexões sobre programação e IA — direto do dia a dia construindo coisas reais.

#meta#blog