Supabase FinOps: 10 Essential Metrics to Stop Overspending in 2026

Supabase FinOps: the 10 metrics that expose overspending on Supabase

Supabase FinOps comes down to one uncomfortable fact: the Spend Cap that most teams treat as their budget control does not cover the line items that make up most of a mature Supabase bill. Compute, read replicas, preview branches, IPv4 addresses, point-in-time recovery and log drains keep billing by the hour whether the cap is on or off. Organizations that overspend on Supabase rarely do so because of one runaway meter; they do so because nobody owns the uncapped lines, nobody measures cost per user, and compute gets upsized every time a query gets slow.

This guide sets out how Supabase pricing actually assembles into an invoice, the mistakes that recur most often in production estates, the signals that tell you that you are overspending, and the ten metrics a Supabase FinOps practice should watch, with the SQL, scheduler jobs and Prometheus queries to measure each one. Prices quoted are list prices from the Supabase pricing page and usage documentation, verified in October 2026. Supabase changes its packaging regularly, so re-check them before you build a budget on them.

Supabase Pricing: How the Invoice Is Actually Built

A Supabase bill has five layers, and they behave very differently. The organization pays a plan fee (Pro from $25 a month, Team from $599). Usage quotas such as monthly active users, egress, storage, Realtime messages and Edge Function invocations are pooled across every project in that organization. Compute is not pooled: every project runs its own Postgres instance, billed per hour from Micro ($0.01344 per hour, roughly $10 a month) to 16XL ($5.12 per hour, roughly $3,730 a month). Paid plans include a $10 compute credit per organization, not per project, and on a paid organization a Nano instance is billed at the Micro rate.

Supabase pricing and Supabase billing anatomy: plan fee, per-project compute, opt-in infrastructure and metered usage, showing which lines the Supabase Spend Cap stops
Figure 1. The five layers of a Supabase invoice. Red boxes keep billing with the Spend Cap enabled.

The distinction that matters for Supabase billing is the one in Figure 1. According to the Supabase cost control documentation, the Spend Cap stops metered usage (disk size, egress, MAU, Realtime, Edge Functions, storage, image transformations, log ingest). It explicitly excludes compute, branching compute, read replica compute, custom domains, provisioned IOPS and throughput, IPv4, log drains, phone MFA and point-in-time recovery, because those are items the customer opts into. The cap also offers no per-item budgets and no cost alerts. In practice, the larger the estate, the larger the share of spend that sits outside the cap.

A worked Supabase pricing example (illustrative)

Consider a common shape for a growing product team: one production project on XL compute, a staging project on Small, three persistent preview branches left running all month, one read replica, IPv4 enabled on the primary, seven days of PITR and one log drain. Using list prices:

Line itemBasisMonthlyStopped by Spend Cap?
Pro planorganization fee$25.00n/a
Production compute (XL)$0.2877/h × 730 h$210.00No
Staging compute (Small)$0.0206/h × 730 h$15.00No
Compute creditper organization−$10.00n/a
Three preview branches (Micro)3 × 730 h × $0.01344$29.43No
Read replica (XL)same size as primary, no credit$210.00No
IPv4primary + replica, $4 each$8.00No
PITR, 7 days$100 per 7 days$100.00No
Log drain$60 per drain$60.00No
Total before any usage overage$647.4396% uncapped

Replica disk (1.25 times the primary disk, billed from the first byte) adds a little more. The point is not the total; it is that $622 of the $647 carries on regardless of the Spend Cap setting. Supabase FinOps therefore starts with ownership of the uncapped lines, not with the usage meters.

Eight Supabase FinOps Mistakes Developers Make Repeatedly

1. Treating the Spend Cap as a budget

The cap protects a hobby project from a surprise egress bill. It does nothing for a team running replicas, branches and PITR, and it cannot alert anyone. A Supabase FinOps practice therefore puts budget alerting outside the platform, fed by the usage page, the upcoming invoice estimate and your own telemetry.

2. Project sprawl and forgotten compute

Each project is its own instance, and the $10 credit is consumed by the first one. Demo projects, abandoned experiments and per-customer proof-of-concept projects each bill compute every hour they exist. Billing is hourly with partial hours rounded up, and it stops only when a project is paused or deleted. A current project inventory is therefore the first Supabase FinOps control.

3. Persistent preview branches

Branching is billed per branch hour from $0.01344, the compute credit does not apply, and the Spend Cap does not cover it. Persistent branches survive after the pull request closes. Ephemeral branches that are merged and deleted are cheap; persistent branches nobody remembers are not. Branch lifecycle belongs in the Supabase FinOps runbook, not in anyone’s memory.

4. Upsizing compute instead of fixing queries

This is the most expensive mistake in absolute dollars. When p95 latency climbs, the dashboard makes a one-click move from Large ($110) to 2XL ($410) easy, adding $300 a month per project, plus the same again on every replica. A sequential scan on a missing index, an RLS policy that calls a function per row, or an N+1 pattern from the client will usually come back after the upsize. Supabase cost optimization begins in pg_stat_statements, not in the compute picker.

5. Selecting every column, every row

The PostgREST pattern select('*') with no range returns every column, including JSONB blobs and embeddings, to every client. Supabase meters uncached egress at $0.09 per GB beyond 250 GB. The egress itself is often modest; the compute spent serializing wide rows is not. The Supabase cost optimization here is a client-side change:

// Before: wide payload, exact count forces a full COUNT(*) on every call
const { data, count } = await supabase
  .from('orders')
  .select('*', { count: 'exact' })

// After: only the columns the screen renders, bounded page, planner estimate for the pager
const { data: page, count: approx } = await supabase
  .from('orders')
  .select('id, status, total_cents, created_at', { count: 'estimated' })
  .eq('customer_id', customerId)
  .order('created_at', { ascending: false })
  .range(0, 49)

6. Realtime fan-out without filters

Supabase counts one Realtime message per client listening to a database change, and a broadcast counts once sent plus once per receiver. A table with 1,000 changes an hour and 10,000 subscribed clients generates 7.2 billion messages a month, roughly $18,000 at $2.50 per million above the 5 million quota (illustrative arithmetic, list price). For Supabase FinOps, Realtime design is a cost decision: filter Postgres Changes subscriptions to the rows a client needs, or move high-fan-out paths to Broadcast channels scoped per room or tenant.

// Subscribe to one room, not the whole messages table
const channel = supabase
  .channel('room-42')
  .on(
    'postgres_changes',
    { event: 'INSERT', schema: 'public', table: 'messages', filter: 'room_id=eq.42' },
    (payload) => render(payload.new)
  )
  .subscribe()

7. Letting disk ratchet upward

On paid plans disk expands by 50% when usage reaches 90%, up to four times in a rolling 24 hours, and it does not shrink during normal operation. It is right-sized only during a Postgres upgrade, to about 1.2 times the database size. A bulk import, a mass UPDATE that doubles a table through dead tuples, or an inactive logical replication slot holding WAL can each trigger a permanent step. Per GB the price is low ($0.125 above 8 GB on general purpose disk), but every replica carries 1.25 times the primary disk and its own provisioned IOPS, so the ratchet multiplies.

Supabase disk autoscaling ratchet: disk grows by 50 percent at 90 percent usage and only right-sizes on a Postgres upgrade, a key Supabase cost optimization risk
Figure 2. Disk autoscaling grows in 50% steps and does not shrink until a Postgres upgrade.

8. Enabling paid add-ons in every environment

PITR at $100 per seven days, a $60 log drain and IPv4 at $4 per database are reasonable for production. Copied into staging, preview and per-developer projects, they become a fixed tax on non-production. IPv4 is only needed for clients that cannot reach IPv6 directly; the Supavisor pooler connection strings serve IPv4 clients without the add-on.

How Do You Know You Are Overspending on Supabase?

In Supabase FinOps, overspending is a ratio, not a number. A $4,000 monthly bill can be efficient for one product and wasteful for another. These are the signals we treat as evidence of overspend in a Supabase FinOps review:

  • Spend per MAU rises while MAU is flat. Unit cost growing faster than usage means cost is coming from configuration, not demand.
  • Compute p95 CPU stays under roughly 30 to 40% on a Large or bigger instance for weeks. The instance is sized for a peak that no longer happens, or for connections rather than CPU.
  • Allocated disk is more than twice the database size. The ratchet has fired on transient data, WAL or bloat.
  • Uncapped lines exceed 80% of the invoice and no named person owns each one.
  • Rows returned per call in pg_stat_statements runs into the thousands for user-facing queries. Clients are paging in the browser.
  • Branch hours exceed the number of open pull requests multiplied by their age. Persistent branches have been orphaned.
  • Replica CPU stays low while the primary is busy. The replica was bought for reads that the application never routed to it.

Each of these Supabase FinOps signals maps to a metric below, and each metric names its source, so a finding can be proven before anyone changes a running system.

Supabase FinOps: The Top 10 Metrics to Monitor

The FinOps Foundation framework organizes the work into Inform, Optimize and Operate. The ten metrics below are the Inform layer for Supabase: they are the minimum evidence set we expect before any right-sizing or architecture decision. Figure 3 shows how they flow from three evidence sources into a weekly decision.

Supabase FinOps and Supabase monitoring architecture: Postgres catalog, Metrics API and organization usage feeding pg_cron snapshots and Prometheus into Inform, Optimize and Operate
Figure 3. A Supabase monitoring loop for FinOps: catalog views, the Metrics API and the usage page.

Metric 1: Unit cost (spend per MAU, per tenant, per 1,000 requests)

The north-star metric of Supabase FinOps. Divide the upcoming invoice estimate by MAU from the organization usage page, and by tenant count if you run a multi-tenant SaaS. Track it weekly. Everything else on this list explains movement in this number.

Metric 2: Compute utilization by instance size

Source: the Supabase Metrics API, a Prometheus-compatible endpoint at https://<project-ref>.supabase.co/customer/v1/privileged/metrics, authenticated with HTTP Basic auth as service_role and a secret API key, scraped every 60 seconds. It is in beta and not available on self-hosted Supabase. Right-sizing decisions should rest on p95 over at least 14 days, not on a single afternoon.

# prometheus.yml (scrape job per project; keep the key in a secret store)
scrape_configs:
  - job_name: supabase_prod
    scrape_interval: 60s
    metrics_path: /customer/v1/privileged/metrics
    scheme: https
    basic_auth:
      username: service_role
      password_file: /etc/prometheus/secrets/supabase_prod_key   # contains ${SUPABASE_SECRET_KEY}
    static_configs:
      - targets: ['${PROJECT_REF}.supabase.co']
        labels: { project: prod, env: production, team: payments }
# p95 CPU busy over 14 days. Confirm series names against one scrape first:
#   curl -s --user "service_role:${SUPABASE_SECRET_KEY}" \
#     https://${PROJECT_REF}.supabase.co/customer/v1/privileged/metrics | grep -E '^node_(cpu|memory)'
quantile_over_time(0.95,
  (1 - avg by (project) (rate(node_cpu_seconds_total{mode="idle"}[5m])))[14d:5m]
)

Metric 3: Connection saturation

Many Supabase upsizes are bought for connection limits, not CPU, because each compute size sets its own max_connections. If CPU is low and connections are near the ceiling, the Supabase cost optimization is the transaction-mode pooler, not a larger instance.

SELECT
    COALESCE(NULLIF(application_name, ''), usename::text) AS client,
    state,
    COUNT(*)                                              AS connections,
    ROUND(100.0 * COUNT(*)
          / current_setting('max_connections')::int, 1)  AS pct_of_max
FROM pg_stat_activity
WHERE backend_type = 'client backend'
GROUP BY 1, 2
ORDER BY connections DESC;

Metric 4: Disk headroom, WAL and replication slot retention

Compare database size to allocated disk (Database Settings in the dashboard), and watch for WAL held by slots, which is the most common silent cause of an unexpected resize.

-- Database size versus WAL on disk
SELECT
    pg_size_pretty(SUM(pg_database_size(datname)))           AS database_size,
    (SELECT pg_size_pretty(SUM(size)) FROM pg_ls_waldir())   AS wal_size
FROM pg_database;

-- WAL retained by each replication slot; an inactive slot with large retention is a resize waiting to happen
SELECT
    slot_name,
    slot_type,
    active,
    pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal
FROM pg_replication_slots
ORDER BY pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) DESC NULLS LAST;

Dropping a slot is destructive: the consumer that owns it (a CDC pipeline, a replica, an ETL tool) loses its position and may need a full resync. Confirm ownership with the team that created it, take a note of the slot definition, and only then run SELECT pg_drop_replication_slot('slot_name');, followed by the retention query above to validate.

Metric 5: Dead tuples and unused index bytes

Bloat and dead indexes consume disk, inflate backup and replica storage, and slow writes. The Supabase CLI wraps several of these checks (supabase inspect db bloat, unused-indexes, table-sizes), and the catalog queries below give the same evidence in SQL.

-- Tables with the most dead tuples relative to live rows
SELECT
    schemaname || '.' || relname                         AS table_name,
    n_live_tup,
    n_dead_tup,
    ROUND(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 1) AS dead_pct,
    last_autovacuum
FROM pg_stat_user_tables
WHERE n_dead_tup > 10000
ORDER BY n_dead_tup DESC
LIMIT 20;

-- Indexes never scanned since statistics were last reset (exclude constraint-backing indexes)
SELECT
    s.schemaname || '.' || s.indexrelname                AS index_name,
    pg_size_pretty(pg_relation_size(s.indexrelid))       AS index_size,
    s.idx_scan
FROM pg_stat_user_indexes AS s
JOIN pg_index AS i
    ON i.indexrelid = s.indexrelid
WHERE s.idx_scan = 0
  AND NOT i.indisunique
  AND NOT i.indisprimary
ORDER BY pg_relation_size(s.indexrelid) DESC
LIMIT 20;

Check pg_stat_database.stats_reset before trusting idx_scan = 0, and check replicas too, because an index unused on the primary may serve reads on a replica. Drop candidates with DROP INDEX CONCURRENTLY in a change window, keeping the CREATE INDEX statement as the rollback.

Metric 6: Egress by service, cached versus uncached

Source: the organization usage page, which breaks egress down by Database, Auth, Storage, Realtime, Edge Functions and the shared pooler, with cached egress (Smart CDN hits) reported separately. The ratio to watch for Storage is cached over total: uncached egress costs three times more per GB than cached ($0.09 against $0.03). For Supabase FinOps reporting, normalize egress per MAU so growth and waste can be told apart.

Metric 7: Rows returned per call

Supabase does not expose response bytes per query in Postgres, so rows per call is the best in-database proxy for payload size. On Supabase, pg_stat_statements lives in the extensions schema.

SELECT
    LEFT(REGEXP_REPLACE(query, '\s+', ' ', 'g'), 120)        AS query_sample,
    calls,
    rows,
    ROUND(rows::numeric / NULLIF(calls, 0), 1)                AS rows_per_call,
    ROUND(total_exec_time::numeric / 1000, 1)                 AS total_exec_seconds,
    ROUND(mean_exec_time::numeric, 2)                         AS mean_ms
FROM extensions.pg_stat_statements
WHERE calls > 100
ORDER BY rows DESC
LIMIT 20;

The same view sorted by total_exec_time is the compute side of the ledger: the queries at the top are what you are paying for when you upsize.

Metric 8: Monthly active users, by type

Supabase counts an MAU as a distinct user who signs in or refreshes a token in the billing cycle; 100,000 are included on Pro, then $0.00325 each. At one million MAU that is $2,925 a month of auth alone. Third-party and SSO MAU are metered separately. Watch for bots, load-test accounts and integration tests that sign in against production, and for clients refreshing tokens for users who no longer use the product. The query below is an approximation to reconcile against the usage page, not a substitute for it.

-- Approximate users active in the last 30 days (sign-in or session refresh)
SELECT COUNT(DISTINCT u.id) AS approx_mau_30d
FROM auth.users AS u
LEFT JOIN auth.sessions AS s
    ON s.user_id = u.id
WHERE GREATEST(u.last_sign_in_at, s.updated_at) >= NOW() - INTERVAL '30 days';

Metric 9: Realtime messages, peak connections and Edge Function invocations

These are the fan-out meters of Supabase pricing. Realtime messages scale with subscribers multiplied by events, so track messages per active client per day. Peak connections above the 500 included on Pro bill at $10 per 1,000. Edge Functions are rarely the expensive line (2 million invocations included, then $2 per million), but a scheduler calling a function every few seconds per tenant can change that.

Metric 10: Uncapped resource hours

An inventory, refreshed weekly, of every project, branch, replica, IPv4 address, PITR window, log drain and custom domain, with an owner and a business reason. This is the Supabase FinOps metric that catches Mistakes 2, 3 and 8 and protects the part of the bill the Spend Cap cannot.

Automating the Evidence: a pg_cron Snapshot for Supabase Monitoring

The catalog metrics above reset on restart or statistics reset, so trends need a history. A small schema and one scheduled function give Supabase FinOps that history. Keep the schema out of the Data API: do not add it to the exposed schemas list, and revoke access from the API roles.

CREATE SCHEMA IF NOT EXISTS finops;
REVOKE ALL ON SCHEMA finops FROM anon, authenticated;

CREATE TABLE IF NOT EXISTS finops.daily_snapshot (
    snapshot_date      date        NOT NULL DEFAULT CURRENT_DATE,
    database_bytes     bigint      NOT NULL,
    wal_bytes          bigint      NOT NULL,
    max_slot_lag_bytes bigint,
    dead_tuples        bigint      NOT NULL,
    unused_index_bytes bigint      NOT NULL,
    storage_bytes      bigint,
    approx_mau_30d     bigint,
    captured_at        timestamptz NOT NULL DEFAULT NOW(),
    CONSTRAINT pk_finops_daily_snapshot PRIMARY KEY (snapshot_date)
);

CREATE OR REPLACE FUNCTION finops.take_snapshot()
RETURNS void
LANGUAGE sql
AS $$
INSERT INTO finops.daily_snapshot (
    database_bytes, wal_bytes, max_slot_lag_bytes, dead_tuples,
    unused_index_bytes, storage_bytes, approx_mau_30d
)
SELECT
    (SELECT SUM(pg_database_size(datname)) FROM pg_database),
    (SELECT SUM(size) FROM pg_ls_waldir()),
    (SELECT MAX(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn))::bigint
       FROM pg_replication_slots),
    (SELECT SUM(n_dead_tup) FROM pg_stat_user_tables),
    (SELECT COALESCE(SUM(pg_relation_size(s.indexrelid)), 0)
       FROM pg_stat_user_indexes AS s
       JOIN pg_index AS i ON i.indexrelid = s.indexrelid
      WHERE s.idx_scan = 0 AND NOT i.indisunique AND NOT i.indisprimary),
    (SELECT SUM((metadata ->> 'size')::bigint) FROM storage.objects),
    (SELECT COUNT(DISTINCT u.id)
       FROM auth.users AS u
       LEFT JOIN auth.sessions AS s ON s.user_id = u.id
      WHERE GREATEST(u.last_sign_in_at, s.updated_at) >= NOW() - INTERVAL '30 days')
ON CONFLICT ON CONSTRAINT pk_finops_daily_snapshot DO NOTHING;
$$;

-- Requires the pg_cron extension (Database > Extensions). Runs 00:15 UTC daily.
SELECT cron.schedule('finops-daily-snapshot', '15 0 * * *', $$SELECT finops.take_snapshot()$$);

-- Verify the job is registered and has run
SELECT jobid, jobname, schedule, active FROM cron.job WHERE jobname = 'finops-daily-snapshot';
SELECT status, start_time, return_message
FROM cron.job_run_details
WHERE jobid = (SELECT jobid FROM cron.job WHERE jobname = 'finops-daily-snapshot')
ORDER BY start_time DESC
LIMIT 5;

Week-over-week growth of database_bytes and wal_bytes forecasts the next disk resize before it happens; a jump in max_slot_lag_bytes is an alert on its own. Run the function once by hand in staging and review its cost on large storage.objects tables before scheduling it in production.

Supabase Cost Optimization: An Operating Model That Holds

Supabase FinOps metrics without decision rights become a dashboard nobody opens. The Supabase FinOps operating model we recommend is deliberately small:

  1. One accountable owner per uncapped line item, recorded next to the resource inventory in Metric 10.
  2. A weekly 30-minute review of unit cost, the top five queries by total execution time, disk headroom and branch hours. Decisions are logged with the metric that justified them.
  3. A rule that compute is upsized only with evidence: p95 CPU or memory pressure over 14 days and a pg_stat_statements review showing no query fix would recover the headroom. Downsizes follow the same evidence in reverse.
  4. Non-production defaults: ephemeral branches, no PITR, no log drains, no IPv4 unless a named client requires it.
  5. Budget alerts outside Supabase, since the platform itself provides no cost alerting today.

Compute changes on Supabase restart the instance, so schedule them in a maintenance window, confirm the rollback size in advance, and watch connection errors and p95 latency for an hour afterwards. As with every recommendation here, test on your own workload before applying it to production, and keep a tested backup and recovery path in place before changing a running database.

Frequently Asked Questions on Supabase FinOps

Does the Supabase Spend Cap stop all charges?

No. It stops metered usage such as egress, MAU, disk size and Realtime messages. Compute, read replicas, branches, IPv4, PITR, log drains, custom domains and provisioned IOPS keep billing.

Is the $10 compute credit per project?

It is per organization, per month, and it does not roll over. It does not apply to read replica or branching compute.

Will deleting data shrink my Supabase disk bill?

Not directly. Deleting data and vacuuming frees space inside the disk, but provisioned disk does not shrink until a Postgres upgrade right-sizes it to about 1.2 times the database size, or until you migrate to a new project.

What is the single biggest Supabase cost optimization lever?

In most estates it is compute: query and index fixes that let a project stay on, or return to, a smaller instance, multiplied across every replica.

Bring In a Second Pair of Eyes

Supabase is Postgres underneath, and most Supabase FinOps wins come from Postgres engineering: plans, indexes, autovacuum, connection pooling and replication. MinervaDB’s PostgreSQL consulting and 24×7 support practice runs measurement-first cost and performance reviews across managed Postgres platforms, the same discipline we apply in our AlloyDB pricing and FinOps guide. If your Supabase bill is growing faster than your users, start with the ten metrics above, and talk to us when you want the evidence reviewed.

References

Supabase pricing · Supabase cost control and Spend Cap · Manage Compute usage · Manage Branching usage · Manage Egress usage · Manage Realtime Messages usage · Manage MAU usage · Manage IPv4 usage · Database size and disk autoscaling · Supabase Metrics API · FinOps Foundation framework

About MinervaDB Corporation 382 Articles
Full-stack Database Infrastructure Architecture, Engineering and Operations Consultative Support(24*7) Provider for PostgreSQL, MySQL, MariaDB, MongoDB, ClickHouse, Trino, SQL Server, Cassandra, CockroachDB, Yugabyte, Couchbase, Redis, Valkey, NoSQL, NewSQL, SAP HANA, Databricks, Amazon Resdhift, Amazon Aurora, CloudSQL, Snowflake and AzureSQL with core expertize in Performance, Scalability, High Availability, Database Reliability Engineering, Database Upgrades/Migration, and Data Security.