Vibe-coded apps look fine in a demo and then quietly become expensive or painfully slow once real data and schedules enter the picture.

A retry loop on a tool error can burn more tokens than the rest of the day’s legitimate work. An N+1 query that was invisible with ten rows becomes a timeout with a thousand. The monthly invoice arrives after the damage is done...

The checks below surface many of those problems while the feature is still under your control. They are small, mostly SQL and a few lines of instrumentation, and they fit the same constraint-first workflow used for the rest of the feature. They are not exhaustive; they are the measurements that are cheap to add early, and that catch the common failure modes before users or the bill notice.

1. Count queries per request as a heuristic

AI agents readily generate the classic N+1 pattern: one query for a list, then one query per item inside a loop or .map. With a handful of rows, the latency is fine. With real data, it is not.

A lightweight client-side counter around supabase.from() is a useful heuristic during development. It will not see every request. PostgREST may generate additional queries, RPCs take a different path, and auth or storage calls are invisible to a wrapper on .from(). Treat a high count as a signal to investigate, not as complete coverage.

let queryCount = 0; const originalFrom = supabase.from.bind(supabase); supabase.from = (...args) => { queryCount++; return originalFrom(...args); }; // after the request or agent turn if (queryCount >= 15) { console.warn(`High query count: ${queryCount}`); // or write to a log table } 

A more durable version logs to a request_metrics table (request_id, query_count, duration_ms, route). After a few real runs, sort by query_count. Anything that scales linearly with the number of rows is a candidate for an IN clause, a join, or an RPC that returns the nested data in one round trip.

Supabase projects already have pg_stat_statements enabled. This query surfaces the same problem from the database side and catches paths the client wrapper misses:

SELECT calls, mean_exec_time, total_exec_time, query FROM pg_stat_statements WHERE calls > 100 AND mean_exec_time > 5 ORDER BY total_exec_time DESC LIMIT 20; 

Repeated near-identical statements with high call counts are the database’s view of an N+1. The Query Performance page in the Supabase dashboard ranks the same data and is usually the fastest place to look first.

2. Log slow statements and their plans (with care)

A query that is acceptable at 50 rows is often unacceptable at 5 000. Slow-query logging is useful while investigating, but the settings need to be targeted.

-- Affects the postgres role for future connections only ALTER ROLE postgres SET log_min_duration_statement = '500ms'; 

Supabase notes that Data API traffic (supabase-js, PostgREST) often uses other roles, so the change above can miss the queries you care about most. Prefer the Query Performance page and role-filtered views for app-generated traffic. For plans on Data API calls, the .explain() modifier (once enabled with appropriate safeguards) is the supported path.

auto_explain can log plans of slow statements, but it is not enabled with a simple LOAD in a managed environment. Supabase documents role-level configuration. Broad, low-threshold plan logging adds overhead and log volume; raise the threshold or disable it again once the investigation is finished.

Look for sequential scans on growing tables, missing indexes on foreign keys used in joins, and sorts that spill to disk. An index that the agent “forgot” is cheaper to add now than after a scheduled job starts timing out.

3. Put a tight budget on every agent run

One of the more expensive failure modes is an agent that treats an error as “try again with more context.” Databricks documented tool-call bugs that silently cost hundreds of thousands of dollars a year because the agent retried instead of failing. Similar loops have produced five-figure bills in a single night. A runaway query or an external API outage can be equally costly; the common pattern is unbounded work that continues without a visible stop.

Enforce the budget in ordinary code, not in the prompt. Before each model or tool call, check:

  • token count (input + output) against a per-run ceiling
  • tool-call count
  • wall-clock time
  • estimated dollar cost (see below)

Use inclusive checks so a run that hits the limit exactly still stops:

const BUDGET = { maxTokens: 30_000, maxToolCalls: 12, maxMs: 90_000 }; function checkBudget(usage) { if (usage.tokens >= BUDGET.maxTokens || usage.toolCalls >= BUDGET.maxToolCalls || usage.elapsedMs >= BUDGET.maxMs) { throw new BudgetExceededError(usage); } } 

Catch the error, log it, and do not let the agent continue. Decide explicitly whether a failed run should be retried (and under what conditions), dead-lettered, or left for manual restart. Idempotency keys help when a partial run is resumed. Provider-side spend limits are a useful account-wide backstop, but they arrive too late to tell you which scheduled job went wrong. The per-run check tells you immediately.

Token counts returned by the model API are reliable for accounting. Converting them to dollars is not. Prices differ by model, cached input tokens are cheaper than fresh ones, output tokens are priced separately, tool-use overhead varies, and rates change. Store the raw token counts and model name; calculate estimated cost only for alerting or dashboards, and treat the number as approximate.

4. Record metrics on every scheduled execution

Once a job is scheduled, you no longer watch it. One practical way to notice drift is to write a metrics row on every run:

CREATE TABLE agent_runs ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), agent_name text NOT NULL, started_at timestamptz DEFAULT now(), duration_ms int, input_tokens int, output_tokens int, model text, tool_calls int, query_count int, estimated_cost_usd numeric(10,6), -- approximate only status text, -- success | budget_exceeded | error | dead_lettered error text ); 

Consider a retention policy. A high-frequency job will grow this table quickly; partition by time or prune older rows if storage cost matters.

A short query then shows the outliers:

SELECT agent_name, percentile_cont(0.95) WITHIN GROUP (ORDER BY estimated_cost_usd) AS p95_cost, max(estimated_cost_usd) AS max_cost, count(*) FILTER (WHERE status != 'success') AS failures FROM agent_runs WHERE started_at > now() - interval '7 days' GROUP BY agent_name; 

Anything whose p95 is several times the median, or whose failure rate is rising, is worth opening. Route alerts to a place a human will actually see (Slack channel, email, or the same destination the successful run uses). A circuit breaker that pauses the schedule after repeated budget or error statuses prevents a bad run from repeating until someone looks.

5. Watch for context and tool-catalog bloat

Every extra tool definition and every previous turn that stays in the context is paid on every subsequent call. Agents that append the full history or register every available MCP tool quickly inflate input tokens. Two simple checks catch this early:

  • Log input token count per turn. A steady climb across turns of the same job usually means history or tool results are being re-sent unnecessarily.
  • Compare the tool list actually sent with the tools the spec says are required. Trim to the minimum set before the run starts.

Sliding windows, summarizing older turns, and loading tools only for the current intent are standard mitigations; the measurement makes it obvious when you need them.

6. Test with realistic volume before you schedule

N+1 and missing indexes stay hidden until the table grows. Before turning a job into a schedule, run it against a copy of production-like data (or a seeded table with thousands of rows). Measure:

  • query count
  • p95 latency
  • token usage
  • whether the budget limits fire

If the numbers are acceptable only because the table is small, fix the query or add the index first. The same measurement harness used in development becomes the baseline the production metrics are compared against.

Putting the checks in the spec

These measurements belong in the same Markdown spec that already constrains the feature:

Performance & cost: - Query count per run ≥ 15 is a signal to investigate (client counter is a heuristic) - p95 duration ≤ 8s on 5k-row fixture - Per-run budget: 30k tokens, 12 tool calls, 90s (inclusive) - Write every run to agent_runs (with retention policy) - Alert on budget_exceeded, repeated errors, or p95 cost > 3× median - On failure: dead-letter or pause schedule; do not auto-retry unbounded 

Because the spec is read by both the interactive agent and the scheduled agent, the limits travel with the code. When a later change introduces a new loop or a wider tool list, the existing checks surface it on the next run instead of on the next invoice.

The tools and model prices will keep changing. The underlying failure modes—unbounded retries, linear query growth, invisible token accumulation—do not. Measuring them with a few counters, targeted slow-query logging, a per-run budget enforced in code, and a simple metrics table is enough to catch many of the expensive cases while the feature is still small enough to fix.