Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

Go Postgres: The Three Drivers, the Connection Pool, the Migration Story, and the Query Time Mistake

Sean

Platform Writer

Jun 23, 2026
8 min read

There are three ways to connect a Go application to Postgres: database/sql with the lib/pq driver (the original, now in maintenance mode), pgx directly (the modern, recommended driver for Postgres), and database/sql with the pgx stdlib wrapper (the modern driver with the standard interface). The right answer is pgx directly for a new project, the connection pool is pgxpool, the migration tool is goose or golang-migrate, and the query-time mistake is reading a million rows into memory without a cursor or a LIMIT.

Go Postgres: The Three Drivers, the Connection Pool, the Migration Story, and the Query Time Mistake

Table of contents

The three driver options

The first option is database/sql with the lib/pq driver. lib/pq was the de facto standard for years; it implements the standard database/sql interface, so the team’s sql.Open / db.Query / db.QueryRow calls work as expected. The gotcha: lib/pq is in maintenance mode (the project is feature-frozen, only security fixes are accepted), and the recommended replacement is pgx.

The second option is pgx directly. pgx is the modern Postgres driver for Go, with a richer feature set than the standard interface (batch queries, COPY protocol, prepared statement cache, type maps for arrays, JSONB, custom types). The right answer is pgx directly for a new project.

The third option is database/sql with the pgx stdlib wrapper. The right answer is the wrapper when the team has an existing database/sql codebase or a library that requires the standard interface.

The connection pool — pgxpool and the right pool size

The pgxpool package wraps pgx with a connection pool. The right answer is pgxpool for any Go service that connects to Postgres — a single connection per request is the wrong default (the connection setup is 10-50ms, the connection holds a Postgres backend process, the team’s Postgres maxes out at 100 connections). The right pool size is the team’s number of concurrent requests divided by the average request duration in seconds.

The right answer for a web service with 100 concurrent requests and a 50ms average duration is a pool of 5 connections. The wrong answer is to set the pool size to 100 because ‘we have 100 concurrent requests’ — the team’s Postgres will run out of backends.

The migration story — goose, golang-migrate, or Atlas

The right answer for migrations is a versioned migration tool: goose, golang-migrate, or pressly/goose. The tool reads a directory of SQL files, applies the migrations in order, and tracks the applied migrations in a schema_migrations table. The team’s CI runs the migration on every deploy; the team’s rollback is migrate down 1.

The gotcha: the team that uses a migration tool but skips the down migration writes forward-only. The right answer is to write the down migration on day one, before the team’s first deploy, so the rollback is automatic.

The query time gotcha — the unbounded result set

The team writes pool.Query(ctx, "SELECT * FROM events"), iterates with rows.Next(), and the production response takes 30 seconds. The team profiles and finds: 10 million rows, no LIMIT, no WHERE clause, the entire result set is being scanned into memory. The right answer is a LIMIT, a WHERE clause, or a cursor.

The right answer for pagination is the keyset pagination pattern (WHERE id > $1 ORDER BY id LIMIT 100), not the OFFSET pattern (OFFSET 10000 is slow for large offsets). The right answer for unbounded result sets is a streaming cursor (pgx.Rows is a streaming cursor by default, database/sql is not).

The connection string — what the DSN actually needs

The Postgres connection string (DSN) is the team’s first point of contact. The right answer is the URL form (postgres://user:pass@host:5432/dbname?sslmode=require&pool_max_conns=10), not the key-value form. The URL form is parseable, the key-value form is harder to copy/paste, and the URL form is what the team’s managed Postgres provider gives in the dashboard.

The gotcha: the DSN that is checked into the repo is a security incident. The right answer is to put the DSN in an env var (DATABASE_URL), load it at startup, and pass it to pgxpool.New(ctx, dsn).

The monitoring — pg_stat_statements and the slow query log

The right answer for monitoring Postgres from a Go service is to enable pg_stat_statements on the database. The team’s Go service logs the query duration, the database logs the per-query stats, the team correlates the two. The right answer for the team’s slow-query alert is to set a threshold (e.g., 100ms) and alert on any query that exceeds it.

The wrong answer is to log every query in the Go service — the log volume is 10x the query volume, the storage cost is high, the signal is in the noise. The right answer is to log only the slow queries.

How this fits the rest of the stack

The infrastructure question is a small piece of a larger pattern: the team’s runtime, storage, database, secret store, logs, and deployment platform are all parts of the same platform. The right answer is to model the full stack before the project ships, not after. The RunxBuild hosting calculator is the right place to do that exercise — pick the runtime, the memory tier, the storage, the secret store, and the egress, and the calculator shows what the deploy actually costs at the team’s actual usage.

Useful related references:

FAQ

What is the best Postgres driver for Go?

pgx directly for a new project, database/sql with the pgx stdlib wrapper for a team that has an existing database/sql codebase.

Should I use lib/pq in 2026?

No. lib/pq is in maintenance mode. The right answer is pgx directly, which has a richer feature set and is actively maintained.

What is the right connection pool size for a Go service?

The right answer is the team’s number of concurrent requests divided by the average request duration in seconds.

What is the right migration tool for Go?

goose, golang-migrate, or ariga/atlas. The right answer is goose for a simple project, golang-migrate for a team that wants the migration to be a CLI tool.

How do I write a Postgres query in Go?

Use pool.Query(ctx, "SELECT ...", args...) and iterate with rows.Next(). Use parameterized queries ($1, $2) to prevent SQL injection.

How do I handle Postgres connection errors in Go?

Use pgxpool with a retry config, log the error, and let the request fail with a 5xx. The right answer for a critical service is a circuit breaker.

How do I read a million rows from Postgres in Go?

Use pgx with rows.Next() and per-row processing — pgx is a streaming cursor by default. Add a LIMIT and paginate for database/sql.

What is the difference between pgx and pgxpool?

pgx is the single-connection driver. pgxpool is a connection pool that wraps pgx. The right answer is pgxpool for any service that handles concurrent requests.

#Go#Postgres#pgx#Tutorial#Database