Gavalas Engineering

Your p99 triples at peak and the database CPU is pinned.

I find where the latency actually lives, then I fix it.

Go backend infrastructure and PostgreSQL internals, at the load where both stop behaving.

Taking one engagement at a time. A 48-hour diagnostic first, then two to six weeks of fixed-scope implementation.

EXPLAIN (ANALYZE)SELECT id, payload FROM events WHERE tenant_id = 4471;Seq Scan on events1.9 s3.7 s5.6 s7.4 s8.2 sRows Removed by Filter: 51,996,816Index Scan using events_tenant_created_idx340 msBuffers: shared hit=946 read=2588p99 latency, checkout query1.2s600ms009:0011:0013:00index deployed

What this usually looks like

A nightly job went sequential across 52 million rows at 02:00, and everything queued behind it until someone killed the query.

The pgx pool exhausts during the spike, handlers sit in Acquire, and p99 stops being query time and becomes queue time.

RDS CPU has been pinned at 95% for six hours. Traffic is flat. Nobody deployed.

A GORM call that was fine at 100k rows is now the top row inpg_stat_statements by total time.

These are planner problems, pooling problems, and access-pattern problems. They look like capacity problems, which is why the usual first response is a bigger instance. That buys a quarter and makes the next diagnosis harder.

Case studies

Why Postgres stops using your index at 52M rows

8.2s340ms

The index existed and the query used its leading column. The planner's row estimate was off by four orders of magnitude, so it costed the index scan higher than reading all 52 million rows.

Raising the statistics target on tenant_id and re-running ANALYZE brought the estimate back in line. No new index, no schema change, no bigger instance.

Reproducible on PostgreSQL 16.4, db.m6g.large, from the seed script in the writeup. Synthetic data, not a client engagement.

Before. The planner estimates one row, then reads all 52 million.
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, payload FROM events
WHERE tenant_id = 4471
  AND created_at >= now() - interval '7 days';

Seq Scan on events  (cost=0.00..1984322.00 rows=1 width=126)
                    (actual time=1.204..8188.417 rows=3184 loops=1)
  Filter: ((tenant_id = 4471) AND (created_at >= now() - '7 days'::interval))
  Rows Removed by Filter: 51996816
  Buffers: shared hit=912 read=1443410
Planning Time: 0.214 ms
Execution Time: 8188.902 ms
After. Corrected statistics. Same query, same index, no schema change.
Index Scan using events_tenant_created_idx on events
                    (cost=0.56..3612.44 rows=3105 width=126)
                    (actual time=0.061..338.902 rows=3184 loops=1)
  Index Cond: ((tenant_id = 4471) AND (created_at >= now() - '7 days'::interval))
  Buffers: shared hit=946 read=2588
Planning Time: 0.188 ms
Execution Time: 339.612 ms

A pgx pool that exhausted at 240 concurrent requests

2.4s96ms

Every handler took a connection for the whole request, including the 300ms it spent waiting on an upstream HTTP call. Under load the pool drained and p99 became queue time, not query time.

Scoping the connection to the query rather than the request, and moving the upstream call outside the transaction, held p99 flat to the pool ceiling.

A GraphQL resolver issuing 1,412 queries per page

1,412 queries4 queries

A nested resolver ran one lookup per parent row, so a 200-item list turned into a fanout the database saw as unrelated traffic. The slow query log showed nothing wrong: every individual query was fast.

Batching the lookups behind a per-request dataloader collapsed the fanout to four statements and took the endpoint off the incident list.

What an engagement is

Performance diagnostic and audit

Every engagement opens here, and it is fixed in scope: 48 hours across yourEXPLAIN ANALYZE trees, pg_stat_statements, the connection pool configuration, and Go runtime profiles taken under load, against read access to a replica.

You get a written findings document that names each bottleneck, the evidence for it, and the specific change that clears it, ranked by what it is worth.

Query plans, indexes, and locks

Index design, statistics targets, partitioning, lock mitigation for the migrations that would otherwise block writes, and rewrites of the handful of queries, GORM orsqlc or hand-written, that hold most of your database time.

The deliverable is a migration you can review, with the before and after plans that justify it.

Go handlers and pooling

Pool sizing and connection lifetime, transaction scope, request fanout, and the handler concurrency that turns a healthy database into a queue and a loaded service into dropped connections.

Usually this is where the latency actually lives once the plans are fixed.

How it runs

Scope
Agreed in writing before anything starts, derived from the diagnostic rather than guessed at. It does not move without both of us saying so.
Timeline
The diagnostic is 48 hours. Implementation runs two to six weeks against two milestones. Longer than that is a hiring problem, not a consulting one.
Deliverables
The findings document, the migrations, the before and after plans, and a written handover. All of it lands in your repository, not in a slide deck.

Who does the work

I am Michael Gavalas, a backend performance engineer. Gavalas Engineering is me, and the person who reads your query plans is the person who writes the migration.

What I do is narrow on purpose: backend performance consulting, PostgreSQL query optimization and Go service reliability. Ten years on backends where the database was the constraint, most of it in Go and Postgres, most of it under load that arrived faster than the schema was designed for.

The case studies above are reproducible from published seed scripts rather than anonymised client logos, because a benchmark you can run yourself is a stronger claim than one you have to take on trust.

Questions buyers ask

What do I get at the end of the 48-hour diagnostic?

A written findings document: the plans as they run today, the row estimates the planner is getting wrong, the pool and runtime numbers under load, and a ranked list of changes with the expected effect of each. It is written to be read by your engineers, not summarised for a slide.

Can you come in while something is actively on fire?

Ask, but the honest answer is usually no. An incident needs whoever already has production context and the pager. What I am useful for is the week after: reading the plans, the lock waits, and the pool metrics that produced it, and making sure the same shape does not repeat.

Where are you based, and how much US overlap do I get?

I work from Greece. That is four hours of daily overlap with US Eastern, every working day, roughly 09:00 to 13:00 Eastern. Standups, incident review, and pairing all happen inside that window, and written handoff covers the rest.

Do you need access to our production database?

No. Read access to a replica and to pg_stat_statements covers the diagnostic. Write access is only needed if you want me to apply the fix rather than hand it over.

Can you do the work without any database access at all?

Partly. With EXPLAIN (ANALYZE, BUFFERS) output, the schema, and pg_stat_statements exports I can identify most planner and indexing problems. Access shortens the loop considerably, but its absence is not a blocker.

What happens if the diagnostic finds nothing in the database?

You get the written findings either way, including the evidence that Postgres is not the bottleneck and where the time is actually going, which is often the Go service in front of it. That is a useful result, and it is why the diagnostic is a standalone piece of work rather than the first week of an implementation.

How do contracts and NDAs work?

I sign your NDA and your contract if you have them, or supply a short one if you do not. Scope, deliverables, and both milestones are agreed in writing before any access is granted.

How soon can you start?

I take one engagement at a time, so the honest answer depends on what is in flight. Send the slow query and I will tell you the real date rather than a hopeful one.

Get in touch

michael@gavalasengineering.com

Send the slow query and the output of EXPLAIN (ANALYZE, BUFFERS) if you have it, or the top five rows of pg_stat_statements by total time if you do not. I read every one.

Based in Greece. Four hours of daily overlap with US Eastern, every working day.