Skip to content

Supabase logs volume: what each request writes, and which Postgres settings add to it

Logs Ingest on Supabase is billed per byte across every service, at $0.50 per GB after 20 GB included on paid plans.1 This page measures where those bytes come from: how much each kind of client request writes to the project logs, and how much the Postgres logging settings a project owner can change add on top. It is for whoever has to explain a logs line on an invoice, or decide which change on their side moves it.

All measured rows below ran on 2026-10-15 from one macOS machine in Singapore (Bun 1.3.14) against throwaway projects in ap-southeast-1 on a Pro-plan organisation, one project per module run, created and read back through the Management API from the same machine; the client actions went straight to the project API. LV01 ran twice (run1, run2) with 100 actions per row; LV02 ran three times (run1 to run3) with 50 requests or statements per condition. The figures are quoted from the RUNLOG of the logs-volume experiment, and redacted copies of the run artifacts are linked in Modules. Rows marked documented come from the public pages cited at the claim and were not run.

Size is a proxy throughout. Metered ingest bytes are not readable with a personal access token (the organisation usage route answered 401, observability surface), so “bytes” here is the length of a row’s event_message plus every key and value in its log_attributes map, as the logs endpoint returns it. The figures compare actions with each other; they do not predict the GB on an invoice.

TL;DR:

  • A REST read or single-row write produced about 0.9 edge_logs rows per request; a REST GET row is 1807 bytes. A 100-row bulk insert produced one row.
  • An Edge Function call with no console output wrote about 3,019 bytes in 2.81 rows: a function_edge_logs row plus a Boot and a Shutdown row in function_logs. Each ~100-character console.log line added about 520-540 bytes.
  • auth.getUser() wrote 2,604 bytes per call; auth.getClaims() with an ES256 token wrote 27 bytes per call (2 rows for 100 calls).
  • Public Storage reads served from the CDN cache are logged: 100 reads with 99 cf-cache-status: HIT produced 99 (run1) and 90 (run2) edge_logs rows.
  • 100 Realtime broadcast messages over an open WebSocket wrote no realtime_logs or edge_logs rows (both runs; run2 0 rows and 2 bytes per message after idle subtraction).
  • On a new project (log_statement=ddl, log_min_duration_statement=-1) REST GETs and SQL select statements wrote 0 postgres_logs rows. Of postgres, authenticator and anon, only settings on authenticator logged REST requests: log_statement=all added 4,405 bytes per REST request and log_min_duration_statement=0 added 9,026 bytes.
  • A role setting on postgres reached new direct sessions at once and had not reached new sessions through the shared pooler 75-453 s later. alter role authenticator reset all returned OK and cleared nothing.

Source in the logsWhat writes itLever on the project sideMeasured here
edge_logsevery HTTP request to the project API, cache hits includedsend fewer requests: client or app caching, Realtime in place of polling, bulk writes, fewer round tripsLV01b-d, LV01h-j
auth_logsAuth server calls such as getUserverify the JWT locally with getClaimsLV01h, LV01i
function_edge_logs, function_logsevery Edge Function invocation, plus the function’s own console outputfewer invocations; remove debug console.logLV01e-g
postgres_logsPostgres itself, per the logging settings in forcereset log_statement, log_min_duration_statement, pgaudit.log where raised; avoid RAISE LOG in hot functionsLV02
pgbouncer_logs, background postgres_logsthe platform, with no client trafficnoneLV01a
realtime_logsnot seen per message in either LV01 run-LV01k, LV01l
client requestAPI gatewayedge_logs ~1.8 KBEdge Functionfunction_edge_logs + Boot + ShutdownRealtime broadcastno rows per messageopen WebSocketPostgREST(authenticator -> anon)Authauth_logs 608 BStorage CDNedge_logs on HITPostgrespostgres_logs: 0 at defaults

Text version of the diagram:

  1. A REST, Auth or Storage request to the project API wrote up to one edge_logs row at the gateway (0.83-0.97 per request; 1807 bytes for a REST GET), including Storage reads served from the CDN cache; an Edge Function call wrote a function_edge_logs row instead.
  2. Behind the gateway, PostgREST logs in as authenticator and switches to anon or authenticated; at defaults Postgres wrote no postgres_logs row for a REST GET (LV02a), and one row per call for a function that runs RAISE LOG (LV02i).
  3. An Auth call writes an auth_logs row (608 bytes for getUser) as well as its gateway row.
  4. An Edge Function call writes a function_edge_logs row and a Boot and a Shutdown row in function_logs, plus one row per console line.
  5. A Realtime broadcast message on an already open WebSocket wrote no realtime_logs or edge_logs row.

Measured, LV01 run2, 100 actions per row (LV01d n = 1), sequential, one project. run1 values in brackets where it measured the same thing. Keys: the legacy anon key for REST GETs, Edge Functions and Realtime; the legacy service_role key for REST inserts and Storage setup; no key for the public Storage reads; a password-signed-in user with an ES256 access token for the Auth rows. Per-action figures for the non-function rows subtract the idle rate from LV01a.

RowActionRows per actionBytes per actionWhere the bytes are
LV01bREST GET, anon key0.91 (0.85)1659 (1520)edge_logs, 1807 bytes per row
LV01cREST insert, service_role key, one row per request0.92 (0.88)1610 (1525)edge_logs
LV01dREST insert, service_role key, 100 rows in one request (n = 1)1 edge_logs row (1)1771 (1771), edge_logs onlyone row in both runs; all sources 2047 (1554), the difference is pgbouncer_logs idle-subtraction noise
LV01hauth.getUser(), signed-in user1.98 (1.89)2604 (2343)edge_logs plus one auth_logs row of 608 bytes
LV01iauth.getClaims(), same user, ES256 token0.03 (0.03)27 (27)1 auth_logs and 1 edge_logs row for the 100 calls
LV01jStorage public object, no key, same object0.97 (1.12)1014 (1144)edge_logs; responses 1 MISS, 99 HIT in both runs; run1 1.12 includes 0.06 late function_logs and 0.06 pgbouncer_logs rows
LV01kRealtime broadcast over an open WebSocket02100 of 100 messages received
LV01eEdge Function, anon key, no console output2.813019function_edge_logs 0.91 rows; function_logs Boot:95 Shutdown:95
LV01fEdge Function, one ~100-character console.log3.883622Boot:97 Log:100 Shutdown:97
LV01gEdge Function, ten console.log lines12.778221Boot:93 Log:1000 Shutdown:93

100 rows written one per request produced 91 edge_logs rows (LV01c, run2); 100 rows written in one request produced 1 (LV01d).

A silent invocation still wrote rows, even when the function printed nothing: a gateway row in function_edge_logs and two lifecycle rows in function_logs, Boot and Shutdown. The 100 sequential calls in LV01e (run2) produced 95 Boot and 95 Shutdown rows, which suggests nearly every call started its own worker (inferred from the event types). Each ~100-character console line then added about 520-540 bytes: LV01f minus LV01e is 1,482 - 940 bytes of function_logs per invocation, and LV01g gives (6,150 - 940) / 10.

Shutdown rows arrived up to about 3.5 minutes after the request (an exploratory probe of 20 invocations with 20 Boot and 20 Shutdown rows, not archived). Counting function_logs by time window therefore undercounts: run1 did that and attributed 0.98 function_logs rows per invocation, and the late rows (event type not recorded in run1) showed up in later windows. run2 counts by log_attributes['function_id'] from the start of each phase, and that is the figure above.

getUser() calls the Auth server, and each call wrote an edge_logs row and an auth_logs row, 2,604 bytes in run2. getClaims() verifies the token locally against a cached JWKS when the project signs with asymmetric keys, and calls the Auth server when it signs with the legacy symmetric secret.2 New projects issued ES256 user tokens here, and 100 getClaims() calls wrote 1 auth_logs and 1 edge_logs row in total. The request path of those two rows was not recorded; a JWKS fetch is what the docs describe. A server that calls getUser() on every request adds about 2,604 bytes to each request (derived from LV01h).

Public Storage reads served from the CDN cache are logged. The same 1 KB object, fetched 100 times with no query string, came back 1 MISS and 99 HIT (cf-cache-status) in both runs and produced 99 (run1) and 90 (run2) edge_logs rows, about 1 KB each: a CDN cache HIT still wrote an edge_logs row. A cache in the client, the application or the project owner’s own CDN keeps the request from reaching the project at all; that follows from LV01b, where each request reaching the gateway is one row, and was not separately measured.

100 broadcast messages over an open WebSocket, all received, wrote no realtime_logs or edge_logs rows (LV01k, both runs), and two subscribed clients idling for 180 s wrote nothing attributable to them (LV01l: every source at or below the idle rate in run2; run1 had 1 edge_logs row and 70 late function_logs rows in the 186 s window). The same updates fetched by polling cost a gateway row per poll: one client polling every 5 seconds sends 17,280 requests a day, which at LV01b’s 1,659 bytes is about 28.7 MB (10^6 bytes) a day, or 0.86 GB over 30 days, per open client (derived from LV01b, not measured as a polling run). Realtime Postgres Changes were not measured.

With no client traffic, a project wrote at a rate of 520 pgbouncer_logs, 40 postgres_logs and 60 function_logs rows an hour (both runs), extrapolated from one 180 s window, about 4.2 MiB a day (run1 4.4). The 3 function_logs rows in that window are probably Shutdown rows from the setup’s warm-up invocations (not determined). No setting for the pgbouncer_logs and postgres_logs background was found (not tested).

Not every request produced a row the run could find. For 100 marked REST GETs, edge_logs held 84 (run1) and 92 (run2) rows, unchanged after a 300 s tail in run2; function_edge_logs counts per 100 invocations were 86-94 across both runs, and Boot counts were 93-97 (run2). Whether that is sampling, loss, or arrival later than the run waited is not determined. Read every per-request row count on this page as covering about 83-97 of every 100 requests (lowest: LV01h run1, 83 edge_logs rows).


The postgres role can change log_statement, log_min_duration_statement and pgaudit.* at role level through supautils; log_connections and log_disconnections are listed as CLI-only project settings.3 The logs-ingest page lists log_statement, log_min_duration_statement, log_connections, pgAudit, and RAISE NOTICE / RAISE LOG in functions as the volume sources a project owner controls.1 On a new project the defaults were log_statement=ddl, log_min_duration_statement=-1, log_min_messages=warning and log_connections=off, and the platform roles supabase_auth_admin, supabase_storage_admin and supabase_admin carry log_statement=none at role level (read on a dev project, not archived).

Measured, LV02 run3, 50 anon REST requests and 50 SQL statements per path per condition. Settings were applied with alter role ... set from a postgres session through the shared pooler (session mode, 5432); REST traffic waited 45 s after each change, longer than PostgREST’s default 30 s db-pool-max-idletime,4 so PostgREST should open a new pool connection (not measured; the platform’s own value was not read). run1 and run2 gave the same REST rows per request except run1 LV02e (0.02); bytes were within 16 bytes per request of run3. The direct path was measured in run3 only.

RowSettingpostgres_logs per REST requestPer SQL statement, direct connectionPer SQL statement, shared pooler (session mode, 5432)
LV02adefaults000
LV02bpostgres log_statement=all01.02 rows, 621 bytes0 (session reported log_statement=ddl)
LV02cauthenticator log_statement=all4.02 rows, 4405 bytes00
LV02danon log_statement=all000
LV02epostgres log_min_duration_statement=001.02 rows, 661 bytes0 (session reported log_min_duration_statement=-1)
LV02fauthenticator log_min_duration_statement=08.1 rows, 9026 bytes00
LV02gpostgres pgaudit.log=all01.02 rows, 681 bytes0 (session reported pgaudit.log=none)
LV02hdefaults, RPC with RAISE NOTICE0--
LV02idefaults, RPC with RAISE LOG1 row, 1336 bytes--

The role that matters for REST requests is authenticator, the role PostgREST logs in as. The same log_statement=all on anon, the role it switches to per request, logged nothing in three runs. Against LV01b’s 1,659 bytes for a REST GET, log_statement=all on authenticator takes a request to about 6,064 bytes (3.7x) and log_min_duration_statement=0 to about 10,685 bytes (6.4x); the ratios combine figures from two modules on different projects and are derived. authenticated, service_role and database-level settings were not tested.

A role setting on postgres reached new direct sessions at once and had not reached new sessions through the shared pooler 75-453 s later. In LV02b, e and g, each “new” pooled session reported the pre-change value of the setting under test, with a backend age that grew by about 75 s per condition across all conditions, from 75 s to 453 s (run2 and run3), which fits one reused server connection, and logged nothing. When the pooler opens a fresh server connection, and the setting with it, was not measured.

alter role authenticator reset all returned OK and left log_statement=all in place, in all three runs; alter role authenticator reset log_statement removed it. At defaults RAISE NOTICE wrote nothing, because log_min_messages=warning sits above it, and RAISE LOG wrote one row per call.


ClaimMeasured or documentedHow it was checked
Per-action rows and bytes by sourcemeasured, LV01 run1 and run2, n = 100 per row (LV01d n = 1; LV01a and LV01l one window each)sequential actions in their own time window, rows per source minus the idle rate, size proxy from the logs endpoint
Edge Function rows by event typemeasured, LV01 run2function_logs filtered on log_attributes['function_id'], grouped by event_type
Shutdown rows arrive up to about 3.5 minutes latemeasured, exploratory, not archived20 invocations on a dev project, polled at 60, 180 and 300 s
Cached Storage reads are loggedmeasured, LV01j, both runscf-cache-status per response against edge_logs rows in the window
Polling at 5 s costs about 28.7 MB a day per clientderived17,280 requests times LV01b’s 1,659 bytes
Postgres settings by role and pathmeasured, LV02, REST three runs, direct path run3, pooler session settings run2 and run3postgres_logs rows in each window; each SQL session read its own current_setting and backend start
reset all clears nothingmeasured, LV02j, three runspg_db_role_setting read after each statement
Metered GB equals the proxynot checkedmetered bytes are not readable by token
Logs Ingest price and the settings a project owner can changedocumented, read 2026-10-15the footnoted pages

None of the behaviours above has been filed upstream as a docs disagreement. Two limits on generalising: every number is one region, one plan tier and sequential single requests on a nearly empty project, so row sizes will differ with longer URLs, headers and statements; and the size proxy tracks what the logs endpoint returns, which the metered byte count may not match.


logs ingest higher than expectedwhich source carries the rows?edge_logs / auth_logs:count requests reaching the projectgatewayfunction_logs:count invocations and console linesfunctionspostgres_logs:read role settings on authenticator and postgresPostgrespgbouncer_logs only:platform background, no leveridle

Text version of the decision guide:

  1. Group the project’s rows by source first; the lever depends on which one carries the volume.
  2. edge_logs and auth_logs: count the requests reaching the project. Per-request getUser (LV01h) and single-row writes (LV01c) are the measured multipliers; polling and refetching on every render follow from LV01b’s row per request and were not measured.
  3. function_logs: count invocations and console lines per invocation. Each ~100-character line is about 520-540 bytes; each silent call is about 3,019 bytes.
  4. postgres_logs: read pg_db_role_setting for authenticator and postgres, and search hot functions for RAISE LOG. At defaults REST GETs and select statements wrote 0 rows here; RAISE LOG wrote 1 row per call.
  5. pgbouncer_logs and a few postgres_logs rows an hour with no traffic are platform background.

PracticeEvidenceModule
Group logs by source before changing anything.Each source has a different lever; the gateway, function and Postgres rows above move independently.LV01, LV02
Replace polling with Realtime where updates are pushed.A REST GET is about 0.9 edge_logs rows and 1,659 bytes; 100 broadcast messages wrote none.LV01 b, k
Batch writes into one request.100 single-row inserts produced 91 edge_logs rows (run2); one 100-row insert produced 1.LV01c, LV01d
Verify auth with getClaims() on projects with asymmetric signing keys.getUser wrote 2,604 bytes per call; getClaims wrote 27 bytes per call (2 rows per 100 calls).LV01h, LV01i
Cache in the client or app; do not rely on the Storage CDN to cut logs.99 CDN cache hits of 100 still produced 90-99 edge_logs rows; client and app caching not measured.LV01j
Gate Edge Function console.log behind a debug flag.Each ~100-character line added about 520-540 bytes; a silent call wrote about 3,019.LV01e-g
Count Edge Function rows by function_id, not by time window.Shutdown rows landed up to about 3.5 minutes late; in run2 the windowed count was 0.98 function_logs rows per call against 1.9 by id.LV01e, run2
Check authenticator role settings when postgres_logs are large.log_statement=all there added 4,405 bytes per request; the same on anon added nothing.LV02 c, d, f
Reset each role setting by name; reset all clears nothing.On authenticator, reset all returned OK and left log_statement=all in place in three runs; other roles not tested.LV02j
Expect pooled sessions to keep an old role setting for a while.Pooled sessions reported the pre-change value with backend ages of 75-453 s; new direct sessions took the new one.LV02b, e, g
Use RAISE LOG only where every call should leave a row.1 row, 1,336 bytes per call; RAISE NOTICE wrote nothing at defaults.LV02h, LV02i
Read per-request row counts as 0.83-0.97 of the requests sent.84 and 92 of 100 marked REST GETs had an edge_logs row; LV01h run1 had 83; unexplained.LV01b, LV01h

Each row links the redacted run artifacts published to out/, in run order. LV02 run1 and run2 ran earlier revisions of the test (no direct path in run1, a failed direct path in run2); the figures and that history are in the experiment’s RUNLOG.

ModuleExperimentTestArtifact
LV01logs-volumelv01-request-footprint.tsrun-2026-10-15T00-48-12-600Z, run-2026-10-15T01-20-10-706Z
LV02logs-volumelv02-postgres-knobs.tsrun-2026-10-15T00-48-17-483Z, run-2026-10-15T01-03-02-840Z, run-2026-10-15T01-20-48-490Z
  1. Supabase, “Manage Logs Ingest usage,” Supabase Docs. https://supabase.com/docs/guides/platform/manage-your-usage/logs-ingest ↩ ↩2

  2. Supabase, “Creating a Supabase client for SSR,” Supabase Docs. https://supabase.com/docs/guides/auth/server-side/creating-a-client ↩

  3. Supabase, “Customizing Postgres configs,” Supabase Docs. https://supabase.com/docs/guides/database/custom-postgres-config ↩

  4. PostgREST, “Configuration,” PostgREST Docs. https://docs.postgrest.org/en/stable/references/configuration.html ↩