Skip to content
Observability

Detecting issues

Detection is the step between accessing project data and troubleshooting a specific problem. Use the sources in Observe the data to produce a count, rate, trend, or named finding. Do not try to prove the root cause yet.

This guide provides starting checks for Health, Security, Performance, and Usage. The log examples use ClickHouse SQL in the Logs Explorer or MCP query_logs. The database examples use Postgres SQL in the SQL Editor or MCP execute_sql.

Use a time range that represents normal traffic, then compare it with the same period after a deployment or configuration change. When a check returns a spike, error code, SQLSTATE, object name, or advisor finding, take that evidence to Diagnosing.

Health#

Health checks answer whether a service is available and behaving within its normal error and resource envelope.

Measure API server-error rate#

Count requests and 5xx responses by hour. A rate is more useful than a raw error count when traffic changes.

select
toStartOfHour(timestamp) as hour,
count() as requests,
countIf(toInt32OrZero(log_attributes['response.status_code']) >= 500) as server_errors,
round(
100.0 * countIf(toInt32OrZero(log_attributes['response.status_code']) >= 500) /
nullIf(count(), 0),
2
) as server_error_percent
from logs
where source = 'edge_logs'
group by hour
order by hour desc
limit 24;

Find failing API paths#

Use the rate check to find an affected window, then identify the paths and status codes producing the errors.

select
log_attributes['request.path'] as path,
toInt32OrZero(log_attributes['response.status_code']) as status,
count() as errors
from logs
where source = 'edge_logs'
and toInt32OrZero(log_attributes['response.status_code']) >= 500
group by path, status
order by errors desc
limit 20;

Check Postgres connection pressure#

Compare active and waiting connections with the configured limit. A high percentage is a signal to inspect pooler settings, long-running transactions, and traffic before changing the limit.

select
count(*) as current_connections,
count(*) filter (where state = 'active') as active_connections,
count(*) filter (where wait_event_type is not null) as waiting_connections,
current_setting('max_connections')::int as max_connections,
round(
100.0 * count(*) / nullif(current_setting('max_connections')::int, 0),
2
) as connection_percent
from pg_stat_activity;

You can read API response errors and service availability in Reports, or use the Metrics API for CPU and connection series. Once you have a failing path, status, or saturated resource, continue in Diagnosing.

Security#

Security checks look for access-control findings and changes in authentication or authorization failures. Treat them as review signals, not proof of an attack.

Measure authorization failures#

Count 401 and 403 responses by hour and status. Compare the rate with a known-good window so normal unauthenticated traffic does not become an alert by itself.

select
toStartOfHour(timestamp) as hour,
toInt32OrZero(log_attributes['response.status_code']) as status,
count() as failures
from logs
where source = 'edge_logs'
and toInt32OrZero(log_attributes['response.status_code']) in (401, 403)
group by hour, status
order by hour desc, status
limit 48;

Find affected paths and methods#

After detecting a spike, group failures by route and method. This separates a broken client flow from failures spread across the API.

select
log_attributes['request.method'] as method,
log_attributes['request.path'] as path,
toInt32OrZero(log_attributes['response.status_code']) as status,
count() as failures
from logs
where source = 'edge_logs'
and toInt32OrZero(log_attributes['response.status_code']) in (401, 403)
group by method, path, status
order by failures desc
limit 20;

Find public-schema tables without RLS#

This database query is a focused inventory check. Confirm each result against the project's intended access model; a result is not evidence that data was exposed.

select
n.nspname as schema_name,
c.relname as table_name
from
pg_class as c
join pg_namespace as n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind in ('r', 'p') and not c.relrowsecurity
order by table_name;

Run Security Advisor from Studio, MCP get_advisors, the CLI, or the Management API for the full catalog of deterministic checks. Take a lint name, table, policy, path, or status pattern to Diagnosing before changing policies, grants, or keys.

Performance#

Performance checks identify expensive work, contention, and cache misses. They narrow the investigation to a query, relation, session, or resource.

Find long-running sessions#

Look for sessions that have been active or idle in a transaction for more than 30 seconds.

select
pid,
usename as role,
state,
now() - query_start as duration,
wait_event_type,
wait_event,
left(query, 120) as query
from pg_stat_activity
where datname = current_database()
and pid != pg_backend_pid()
and state in ('active', 'idle in transaction')
and now() - query_start > interval '30 seconds'
order by duration desc
limit 20;

Find blocked sessions#

Use pg_blocking_pids to name the blocked and blocking processes. Do not cancel either process until you understand the transaction and its impact.

select
blocked.pid as blocked_pid,
blocked.usename as blocked_role,
blocker.pid as blocking_pid,
blocker.usename as blocking_role,
now() - blocked.query_start as blocked_for,
left(blocked.query, 120) as blocked_query,
left(blocker.query, 120) as blocking_query
from pg_stat_activity as blocked
cross join lateral unnest(pg_blocking_pids(blocked.pid)) as blocking_pid
join pg_stat_activity as blocker on blocker.pid = blocking_pid
order by blocked_for desc;

Find expensive query patterns#

pg_stat_statements aggregates normalized queries over time. Rank by total execution time, then inspect mean time and calls before deciding whether a frequent query is inefficient.

select
calls,
round(total_exec_time::numeric, 2) as total_time_ms,
round(mean_exec_time::numeric, 2) as mean_time_ms,
rows,
left(query, 160) as query
from pg_stat_statements
order by total_exec_time desc
limit 20;

Measure shared-buffer hit rate#

A ratio below 99% means more than 1% of observed block accesses missed shared_buffers. Postgres cannot tell whether a miss was served by the operating system cache or physical disk.

select
'index hit rate' as name,
round(100.0 * sum(idx_blks_hit) / nullif(sum(idx_blks_hit) + sum(idx_blks_read), 0), 2) as ratio
from pg_statio_user_indexes
union all
select
'table hit rate' as name,
round(
100.0 * sum(heap_blks_hit) / nullif(sum(heap_blks_hit) + sum(heap_blks_read), 0),
2
) as ratio
from pg_statio_user_tables;

Pull Performance Advisor findings and compare the same window with Reports or the Metrics API. The full command and SQL catalog is in Inspect the database.

Usage#

Usage checks identify growth in traffic, data, and connections before it becomes a capacity problem. They do not calculate billing totals.

Trend API requests#

Count requests by hour to establish a baseline and spot step changes.

select
toStartOfHour(timestamp) as hour,
count() as requests
from logs
where source = 'edge_logs'
group by hour
order by hour desc
limit 168;

Find high-volume API paths#

Group by method and path to identify which workload accounts for the growth.

select
log_attributes['request.method'] as method,
log_attributes['request.path'] as path,
count() as requests
from logs
where source = 'edge_logs'
group by method, path
order by requests desc
limit 20;

Find the largest relations#

Measure tables and their indexes together. Save the result on a regular cadence to establish a growth trend.

select
schemaname,
relname as table_name,
pg_total_relation_size(relid) as total_bytes,
pg_size_pretty(pg_total_relation_size(relid)) as total_size
from pg_catalog.pg_statio_user_tables
order by total_bytes desc
limit 20;

Count connections by role and state#

Connection growth can reveal a new workload or a client that is not pooling correctly.

select
usename as role,
state,
count(*) as connections
from pg_stat_activity
where datname = current_database()
group by role, state
order by connections desc;

Reports show request, disk, and database-size trends without SQL. The Management API usage endpoint returns request counts for authorized scripts. Use supabase inspect db table-sizes and bloat to run related database checks from the CLI.

Turn a detection into a diagnosis#

A detection result should name an affected time window and at least one concrete anchor: a path, status, SQLSTATE, request ID, query, relation, PID, policy, or advisor lint. Take that evidence to Diagnosing, identify the cause, apply the smallest relevant solution, and rerun the same detection check to verify the result.

After a check is useful and repeatable, hire an agent to run it on a schedule.