---
title: "database/sql em Go: Pool de Conexões, MaxOpenConns e Produção"
url: "https://golang.com.br/blog/database-sql-go-pool-conexoes-producao/"
markdown_url: "https://golang.com.br/blog/database-sql-go-pool-conexoes-producao.MD"
description: "Aprenda o pool de conexões do database/sql em Go: SetMaxOpenConns, SetMaxIdleConns, SetConnMaxLifetime, timeouts, prepared statements e armadilhas de produção com MySQL, PostgreSQL e SQLite."
date: "2026-06-22"
author: "Golang Brasil"
---

# database/sql em Go: Pool de Conexões, MaxOpenConns e Produção

Aprenda o pool de conexões do database/sql em Go: SetMaxOpenConns, SetMaxIdleConns, SetConnMaxLifetime, timeouts, prepared statements e armadilhas de produção com MySQL, PostgreSQL e SQLite.


O pacote `database/sql` da biblioteca padrão de Go traz um pool de conexões embutido. Muita gente acha que está usando o driver diretamente, mas desde a primeira linha de `sql.Open` o que roda é um pool que gerencia conexões, bloqueia chamadas quando o limite estoura e reutiliza sessões entre goroutines. O problema é que os defaults foram escolhidos para um notebook de desenvolvimento e não para um serviço sob carga. Em produção, conexões ociosas derrubadas por proxy, queries presas, `too many connections` no banco e latência irregular quase sempre vêm de três linhas de configuração que ninguém ajustou.

Este guia mostra como o pool do `database/sql` funciona, como dimensionar `SetMaxOpenConns`, `SetMaxIdleConns` e `SetConnMaxLifetime` para o seu workload real e quais armadilhas matam serviços em pico. Ele complementa o [guia de pgxpool para PostgreSQL](/blog/pgxpool-go-postgresql-producao/), o [guia de transações, locks e retry](/blog/postgresql-transacoes-go-locks-retry/), o [guia de migrations](/blog/migrations-go-banco-dados-producao/) e o [guia de timeouts e cancelamento com context](/blog/context-timeout-cancelamento-go/).

## Como o pool do database/sql funciona

`sql.DB` não é uma conexão. É um pool que mantém zero ou mais conexões reais a um banco. Cada chamada de `Query`, `Exec` ou `Begin` pega uma conexão do pool, executa e devolve. Se todas as conexões estiverem em uso e o limite de conexões abertas ainda não foi atingido, o pool abre uma nova. Se o limite foi atingido, a chamada fica bloqueada esperando uma conexão livre — ou cancelada pelo contexto, se você passou um.

```go
db, err := sql.Open("pgx", dsn)
if err != nil {
    return err
}
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5 * time.Minute)
db.SetConnMaxIdleTime(2 * time.Minute)
```

A diferença entre `sql.Open` e `db.Ping` é o ponto onde muita gente descobre que o DSN estava errado: `Open` só valida o formato da string, sem tocar no banco. Sempre chame `PingContext` na inicialização do serviço para falhar cedo, antes que o [graceful shutdown](/blog/graceful-shutdown-go-producao/) precise lidar com isso sob carga.

## Os três parâmetros que importam

### SetMaxOpenConns

Limita o número máximo de conexões abertas simultaneamente, incluindo as em uso e as ociosas. O default é ilimitado, o que em produção vira uma bomba: cada goroutine de handler que precisa do banco abre uma conexão, e em um pico de 2 mil requisições concorrentes você cria 2 mil conexões. O banco recusa com `too many connections`, o proxy na frente corta conexões ociosas, e a aplicação passa a abrir e fechar conexões numa frenesi que derruba a latência.

O número certo não é "o máximo que o banco aguenta". É o ponto onde a vazão para de crescer — geralmente entre 2x e 4x o número de cores do banco de dados, ou o limite definido pelo DBA. Para PostgreSQL, valores típicos ficam entre 10 e 50 por instância de aplicação; para MySQL, entre 20 e 100. Meça: o que importa é a relação entre conexões abertas, latência de query e throughput. Se dobrar o pool não aumenta a vazão, o gargalo está em outro lugar (lock de linha, índice faltante, rede) e adicionar conexões só piora.

### SetMaxIdleConns

Limita conexões ociosas mantidas no pool. O default é 2. Isso significa que se o serviço recebe rajadas de tráfego, a cada rajada o pool abre novas conexões (pagando o custo de handshake TLS e autenticação) e depois fecha o excesso. Em workloads com variação de carga isso vira um ciclo de abrir e fechar que domina o perfil.

A regra prática: `SetMaxIdleConns` deve ser igual a `SetMaxOpenConns` na maioria dos casos. Assim, quando o tráfego cai, as conexões ficam ociosas esperando a próxima rajada em vez de serem destruídas. O custo de manter 25 conexões ociosas é trivial perto do custo de reabri-las a cada pico.

### SetConnMaxLifetime e SetConnMaxIdleTime

`SetConnMaxLifetime` limita o tempo total de vida de uma conexão, mesmo que esteja em uso constante. O default é ilimitado. Em produção isso é perigoso porque proxies (HAProxy, AWS RDS proxy, Cloud SQL Auth Proxy) e firewalls costumam cortar conexões paradas ou longas sem avisar a aplicação. O sintoma é o temido `EOF`, `connection reset by peer` ou `write: broken pipe` em uma query que funcionava segundos antes.

Defina `SetConnMaxLifetime` para algo menor que o timeout do proxy — geralmente 5 minutos é seguro. `SetConnMaxIdleTime`, mais agressivo, fecha conexões ociosas individualmente, útil quando o tráfego cai de verdade (por exemplo, de madrugada) e você não quer sustentar o pool cheio.

## Prepared statements e o cache de conexões

O `database/sql` cacheia prepared statements por conexão. Quando você chama `db.Prepare`, ele cria um prepared statement em cada conexão sob demanda — a primeira vez que uma conexão executa aquela query. Isso tem duas consequências. Primeiro, em pools grandes, a primeira rajada pode disparar dezenas de prepares simultâneos, o que sobrecarrega o banco. Segundo, prepared statements retêm estado na conexão (sessões, variáveis temporárias), o que complica o reuso.

Para queries executadas milhões de vezes, prepared statements valem a pena. Para queries raras ou dinâmicas, prefira `db.QueryContext` direto, que prepara implicitamente e descarta. O [guia de sqlc](/blog/sqlc-go-postgresql-typesafe/) mostra uma alternativa que gera código tipado a partir de SQL, eliminando esse dilema.

## Context, timeouts e o efeito em cascata

Nunca chame `db.Query` sem contexto em produção. A versão sem contexto não respeita cancelamento: se o banco travar, a goroutine fica presa para sempre, segurando uma conexão do pool. Em poucos minutos, todas as conexões estão bloqueadas e o serviço inteiro para de responder.

```go
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()

rows, err := db.QueryContext(ctx, "SELECT id, nome FROM usuarios WHERE ativo = $1", true)
if err != nil {
    if errors.Is(err, context.DeadlineExceeded) {
        return ErrTimeoutBanco
    }
    return err
}
defer rows.Close()
```

O timeout deve ser definido no nível certo. Não use um único timeout global de 30 segundos para tudo: queries de dashboard podem aceitar 10 segundos, mas uma query em hot path de login precisa voltar em 200ms. Configure timeouts por rota, sempre usando o contexto da requisição como base, e deixe o [guia de context, timeout e cancelamento](/blog/context-timeout-cancelamento-go/) explicar o padrão completo.

Uma armadilha comum: passar o contexto errado para o `BeginTx`. Se o contexto expira durante uma transação, o Go faz rollback automaticamente, mas a transação pode ter efeitos colaterais visíveis — como locks liberados, outbox parcialmente escrito. Para transações críticas, combine timeout com o [padrão outbox](/blog/outbox-pattern-go-eventos-confiaveis/) e [idempotência](/blog/idempotencia-retry-dlq-go/) para que retries sejam seguros.

## Fechando Rows,Stmt e transações

Toda chamada de `Query` devolve `*sql.Rows`. Se você não chamar `rows.Close()`, a conexão não volta para o pool e vira um vazamento silencioso. Em serviços de longa duração isso se manifesta como o pool secando e todas as chamadas travando. A regra é simples: sempre `defer rows.Close()`. O custo de fechar duas vezes é zero (a segunda é no-op), o custo de não fechar é um incidente.

```go
rows, err := db.QueryContext(ctx, "SELECT ...")
if err != nil {
    return err
}
defer rows.Close()

for rows.Next() {
    var u Usuario
    if err := rows.Scan(&u.ID, &u.Nome); err != nil {
        return err
    }
    // ...
}
if err := rows.Err(); err != nil {
    return err
}
```

`rows.Err()` depois do loop é o detalhe que muita gente esquece: `Next` retorna `false` no fim OU no erro, e só `Err()` distingue os dois.

## Monitorando o pool em produção

O `database/sql` expõe métricas via `db.Stats()` que valem ouro:

- `OpenConnections`: conexões abertas no momento.
- `InUse`: conexões executando queries.
- `Idle`: conexões ociosas.
- `WaitCount`: número de chamadas que precisaram esperar por conexão.
- `WaitDuration`: tempo total esperando conexões.
- `MaxIdleClosed` e `MaxLifetimeClosed`: conexões fechadas por política do pool.

Exponha essas métricas no Prometheus — o [guia de OpenTelemetry e observabilidade](/blog/go-opentelemetry-observabilidade-tracing-metricas/) mostra como instrumentar a aplicação. Os sinais de problema são claros: `WaitCount` crescendo indica que o pool é pequeno demais; `MaxIdleClosed` alto indica que `SetMaxIdleConns` está abaixo do tráfego real; `MaxLifetimeClosed` estável é saudável, mas se `OpenConnections` bate em `MaxOpenConns` e `WaitDuration` cresce, o banco ou o pool estão saturados.

## database/sql vs driver específico

Para PostgreSQL em serviços sérios, [pgxpool](/blog/pgxpool-go-postgresql-producao/) costuma ser melhor que `database/sql` com o driver `pgx`: o pgx nativo fala o protocolo binário, suporta tipos customizados, `COPY` e batching que o `database/sql` não expõe. Para MySQL, o driver `go-sql-driver/mysql` sobre `database/sql` é o padrão da indústria. Para SQLite, `modernc.org/sqlite` (puro Go) ou `mattn/go-sqlite` (CGO) — o detalhe crítico aqui é que SQLite só permite um escritor por vez, então `SetMaxOpenConns(1)` é comum para o modo WAL com leitores paralelos.

A regra de ouro: comece com `database/sql` pela portabilidade, mude para um driver específico quando precisar de recursos que a interface abstrai fora. O [guia de migrations](/blog/migrations-go-banco-dados-producao/) mantém o schema portável independente do driver escolhido.

## Checklist de produção

- Defina `SetMaxOpenConns` para um valor que faça sentido para o banco, não ilimitado.
- Iguale `SetMaxIdleConns` a `MaxOpenConns` para evitar reopen em rajadas.
- Defina `SetConnMaxLifetime` menor que o timeout do proxy de rede.
- Defina `SetConnMaxIdleTime` para fechar conexões em horários de baixa.
- Sempre `defer rows.Close()` e cheque `rows.Err()` após o loop.
- Sempre use a versão `Context` de cada método com timeout apropriado.
- Exponha `db.Stats()` como métricas e alerte sobre `WaitDuration` crescendo.
- Rode com [pgxpool](/blog/pgxpool-go-postgresql-producao/) se quiser extrair o máximo do PostgreSQL.

O pool do `database/sql` é uma das peças mais subestimadas da biblioteca padrão de Go: funciona bem por padrão no notebook e falha silenciosamente em produção. Os três parâmetros certos removem 90% dos incidentes de banco que afligem serviços Go em escala. Para quem mantém backends em mais de uma linguagem, o padrão de configurar explicitamente pool, lifetime e timeout é universal — vale comparar com a abordagem do <a href="https://python.dev.br/blog/golang-erros/" target="_blank" rel="noopener noreferrer" onclick="umami.track(portfolio-site-click, { destination: python.dev.br })">Python Dev Brasil sobre performance e concorrência em backends</a> ao desenhar pools entre stacks.
