---
title: "Пул соединений pgxpool"
description: "Пул соединений pgxpool: pgxpool.New, MaxConns и MinConns, жизненный цикл коннектов, Ping перед работой, pool.Stat и расчёт лимитов под max_connections."
course: "PostgreSQL в Go: драйвер pgx с нуля до продакшена"
url: "https://golangmaster.ru/courses/pgx/lessons/04-pool-soedinenij/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 20
---

# Пул соединений pgxpool

*Курс «PostgreSQL в Go: драйвер pgx с нуля до продакшена», урок 4 из 25. ~20 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/pgx/lessons/04-pool-soedinenij/*

pgxpool — пул соединений, без которого не обходится ни один Go-сервис с Postgres: он держит несколько готовых коннектов, выдаёт их горутинам по запросу и следит за жизненным циклом каждого. Разберём, зачем пул нужен, как настроить MaxConns и MinConns под лимиты базы, почему Ping входит в стартовую проверку сервиса и как pool.Stat показывает, что происходит внутри.

Соединение с Postgres — дорогая штука, и не только из-за времени на TCP-хендшейк, хотя он тоже стоит миллисекунд. На каждый коннект сервер создаёт отдельный процесс — backend, — который держит несколько мегабайт памяти и живёт, пока жив клиент. Десяток соединений сервер не заметит, сотня — уже серьёзная нагрузка на память и планировщик, а тысяча одновременных попыток подключения способна уложить маленький инстанс раньше, чем выполнится хоть один запрос. Go-сервис при этом многогорутинный: каждый HTTP-запрос хочет свою транзакцию, и наивное открытие соединения на каждый запрос перемножает обе проблемы. Пул решает обе сразу: соединения создаются заранее и в ограниченном количестве, переиспользуются сотнями горутин, а нехватка превращается в короткое ожидание внутри приложения вместо шторма подключений к серверу. В pgx пул — не дополнение из сторонних библиотек, а часть основного модуля: подпакет pgxpool с API, повторяющим одиночное соединение один в один. Арифметика сервера простая и суровая: дефолтный max_connections равен сотне, из них пара слотов зарезервирована суперпользователям на спасение при исчерпании, а ещё несколько уйдёт мониторингу и миграциям. Два сервиса по десять подов — уже двадцать инстансов, и каждому MaxConns не может быть выше четвёрки. Отсюда правило, которое я повторяю на каждом код-ревью сервиса с базой: размер пула — глобальный бюджет, распределённый по инстансам, а не локальная настройка одного приложения. Пул, кстати, не координируется с соседними инстансами: каждый живёт своим бюджетом, и сумма сходится только в вашей голове или в конфиге деплоя.

### pgxpool.New: замена одной строки

Переезд с одиночного соединения на пул минимален: pgx.Connect превращается в pgxpool.New, тип *pgx.Conn — в *pgxpool.Pool, а conn.Close(ctx) — в pool.Close(). Всё остальное API совпадает: pool.Exec, pool.Query, pool.QueryRow и pool.Begin работают так же, как у одиночного соединения, поэтому первые уроки курса проходят без переписывания кода. Создание пула ленивое: New только разбирает конфиг, а соединения открываются по мере необходимости — до MinConns в простое и до MaxConns под нагрузкой. Ошибка вернётся сразу, если строка подключения битая, и позже — если база недоступна: поэтому пул пингуют при старте, чтобы поймать проблему там, где она есть, а не в первом запросе пользователя. Ленивость при этом не значит «никогда»: фоновая горутина пула сама дотягивает число соединений до MinConns, поэтому сразу после запуска в pg_stat_activity видно ровно столько сессий приложения, сколько обещала настройка — тёплый старт без ручной подготовки. Ещё одна деталь, которую удобно знать заранее: DSN пула — та же строка из третьего урока, плюс параметры с префиксом pool_, так что конфигурация не раздваивается.

### Пул с лимитом из строки подключения

```go
package main

import (
        "context"
        "fmt"
        "log"

        "github.com/jackc/pgx/v5/pgxpool"
)

func main() {
        ctx := context.Background()

        pool, err := pgxpool.New(ctx,
                "postgres://app:secret@localhost:5432/app?pool_max_conns=10")
        if err != nil {
                log.Fatal(err)
        }
        defer pool.Close()

        if err = pool.Ping(ctx); err != nil {
                log.Fatal(err)
        }

        tag, err := pool.Exec(ctx,
                "CREATE TABLE IF NOT EXISTS notes (" +
                "id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, title text)")
        if err != nil {
                log.Fatal(err)
        }

        fmt.Println("готово:", tag.String())
}
```

Пул создаётся из DSN с параметром pool_max_conns=10 — потолок одновременных соединений задаётся прямо в строке. Ping при старте ловит недоступную базу до первого запроса, а не посреди вечера. Exec выполняет DDL — таблица notes станет полигоном для INSERT и SELECT в пятом уроке. pool.Close() освобождает все соединения при выходе из программы.

### MaxConns, MinConns и жизненный цикл коннектов

Настройки пула живут в структуре pgxpool.Config, и их немного. MaxConns — потолок одновременных соединений: по умолчанию большее из чисел четыре и количества ядер процессора. MinConns — сколько коннектов пул держит даже в простое: ноль по умолчанию, то есть проснувшийся после ночи простоя сервис заново греет соединения на первых запросах. MaxConnLifetime — максимальный возраст соединения, дефолт час: по истечении коннект закрывается и открывается свежий, что спасает после перезапусков базы и переподключений за балансировщиками. MaxConnIdleTime — сколько простаивающий коннект живёт без дела, дефолт тридцать минут: незанятые соединения закрываются, чтобы не занимать серверную память впустую. Практическое следствие: скрипт, который раз в час делает один запрос, каждый раз получает холодное соединение и платит хендшейком — такому удобнее одиночный Conn, чем пул с горячими коннектами, которые всё равно умрут от простоя. HealthCheckPeriod — как часто пул проверяет здоровье коннектов, по умолчанию минута. К lifetime настраивается и добавка-джиттер, MaxConnLifetimeJitter: добавьте её, чтобы соединения не умирали синхронной волной после одного и того же момента — без джиттера рестарт базы уронил бы и переоткрыл все коннекты разом. Все значения задаются и из DSN: pool_max_conns, pool_min_conns, pool_max_conn_lifetime, pool_max_conn_idle_time, pool_health_check_period — код конфигурации нужен, только когда числа считаются динамически из окружения. Ещё одна деталь механики: соединение считается занятым не на время запроса, а на всю транзакцию — Begin держит коннект до Commit или Rollback. Пул, где половина горутин сидит в длинных транзакциях, испытывает дефицит даже при скромном темпе запросов: для пула одна долгая транзакция хуже сотни коротких. Если AcquiredConns всё время прилип к потолку, ищите не настройку MaxConns, а транзакции без конца.

### Параметры pgxpool: DSN, поле Config и дефолт

| Параметр в DSN | Поле Config | Дефолт | За что отвечает |
| --- | --- | --- | --- |
| pool_max_conns | MaxConns | max(4, NumCPU) | потолок одновременных соединений |
| pool_min_conns | MinConns | 0 | минимум соединений в простое |
| pool_max_conn_lifetime | MaxConnLifetime | 1 час | максимальный возраст соединения |
| pool_max_conn_idle_time | MaxConnIdleTime | 30 минут | максимальный простой незанятого коннекта |
| pool_health_check_period | HealthCheckPeriod | 1 минута | период фоновых проверок здоровья |

> **Что сломается, если не считать MaxConns.** Классика дежурств: десять подов сервиса по двадцать соединений — и на базе кончается лимит max_connections, который по умолчанию равен сотне. Новые подключения получают FATAL: sorry, too many clients already, readiness-пробы краснеют, деплой останавливается, а ручной psql в контейнер тоже не проходит — все слоты заняты вашим же сервисом. Считайте всегда: сумма MaxConns всех инстансов плюс запас для миграций, админ-сессий и мониторинга должна быть меньше max_connections. Десять подов — значит пять-семь соединений на каждый, а не двадцать.

### Настройка пула из кода

```go
cfg, err := pgxpool.ParseConfig(os.Getenv("DATABASE_URL"))
if err != nil {
        log.Fatal(err)
}

cfg.MaxConns = 8
cfg.MinConns = 2
cfg.MaxConnLifetime = 30 * time.Minute
cfg.MaxConnIdleTime = 5 * time.Minute

pool, err := pgxpool.NewWithConfig(ctx, cfg)
```

ParseConfig разбирает строку подключения вместе с параметрами пула, после чего поля можно переопределить из кода — удобно, когда значения считаются из размеров окружения: числа подов, переменных деплоя. NewWithConfig собирает пул из готовой конфигурации и возвращает ту же структуру, что и New. Заметьте типы: MaxConns — int32, время — time.Duration.

### Шесть правил настройки пула

- Посчитайте: сумма MaxConns всех инстансов плюс запас из 10-20 соединений меньше max_connections базы
- MinConns 2-5 для сервиса с ровным трафиком: не греть соединения после каждого простоя
- MaxConnLifetime меньше таймаутов балансировщиков и файрволов: соединение умрёт раньше, чем их отрежут
- MaxConns выше числа ядер базы почти не даёт прироста: Postgres масштабирует CPU, а не процессы
- Не ставьте -1 (без лимита): шторм соединений на пике положит базу раньше, чем вы это заметите
- Меряйте pool.Stat() до и после изменения настроек: числа вместо ощущений

### Ping перед работой: старт сервиса и readiness

Две ситуации, где Ping обязателен. Первая — старт: пул ленивый, и без проверки сервис поднимется зелёным даже с недоступной базой, а ошибка приедет с первым пользовательским запросом. Пинг после создания пула превращает эту ошибку в быстрый и понятный крах деплоя — fail fast в чистом виде. Вторая — readiness-проба Kubernetes или health-эндпоинт: периодический pool.Ping(ctx) показывает балансировщику, жива ли база, а не сам процесс сервиса. Таймаут задаётся контекстом, а не параметром: оберните вызов в context.WithTimeout на пару секунд, чтобы проба не зависла на недоступной сети — подробности в пятнадцатом уроке про контексты и таймауты. Полезная деталь: Ping берёт соединение из пула как обычный запрос, так что постоянные пинги каждую секунду сами по себе нагрузки не создают, но и пользы сверх health-проб от них нет. Кстати, Ping при пустом пуле сам открывает соединение: первый пинг после старта — заодно и прогрев. В readiness-эндпоинте это выглядит как три строки обработчика: контекст с таймаутом на пару секунд, pool.Ping и выбор кода ответа по ошибке. Главное — не путать эту пробу с liveness: живой без базы сервис продолжает отвечать ошибками приложения, и убивать его рестартом бессмысленно.

### pool.Stat(): сколько коннектов занято прямо сейчас

Пул знает о себе всё, и pgxpool.Stat() отдаёт это наружу методами: TotalConns — сколько соединений живёт, AcquiredConns — сколько занято горутинами, IdleConns — сколько простаивает, MaxConns — потолок. Отдельно полезны AcquireCount — сколько раз горутины брали коннект, и EmptyAcquireCount — сколько раз запрос на соединение застал пул пустым и ждал: растущий EmptyAcquireCount — первый симптом нехватки MaxConns, задолго до жалоб пользователей на медленные ответы. Я вывожу эти числа в метрики на старте сервиса и смотрю график перед любым тюнингом: пул, настроенный по ощущениям, исправляется за один взгляд на TotalConns против MaxConns. Собирайте Stat периодически, раз в десять-пятнадцать секунд, а не на каждый запрос: график гладкий, накладные расходы незаметны. Здоровая картина, к которой я стремлюсь: IdleConns около MinConns в спокойные часы, AcquiredConns на треть ниже потолка даже в пик, EmptyAcquireCount около нуля. Отклонения читаются однозначно: растущий Idle при нулевом Acquired — пул больше, чем нужно; прилипший Acquired — либо мало соединений, либо длинные транзакции. Вынос в Prometheus и алерты — тема двадцать четвёртого урока, а здесь — привычка: перед каждым изменением настроек запишите текущие значения Stat, иначе сравнить будет не с чем.

Кейс, показывающий цену одного числа. Ночной джобой миграций команда запускала пересчёт агрегатов, который открывал собственные соединения в обход пула сервиса — по двадцать на инстанс. В ночь большого релиза сервис упёрся в пик трафика, джоб стартовал на всех подах одновременно, и пул запросов исчерпался: горутины стояли в очереди за коннектом, p95 уехал за секунды, но всё продолжало отвечать — пустой пул ждёт, а не роняет запросы. Диагноз поставился не по коду, а по Stat: EmptyAcquireCount рос тысячами в минуту, TotalConns прилип к MaxConns. Починили двумя строками: отдельный лимит для джоба из общего расчёта и метрика на EmptyAcquireCount с алертом. С тех пор правило команды звучит так: любой новый компонент, которому нужна база, объявляет свой лимит явно и входит в общий расчёт до деплоя, а не после первого инцидента. Вывод кейса: пул не падает, он тихо замедляет — увидеть это можно только числами. И вторая половина морали: компонент, открывающий соединения мимо общего пула, обязан считаться в том же бюджете max_connections, иначе бюджет лживый.

### Снимок состояния пула

```go
stat := pool.Stat()

fmt.Printf("соединений: %d из %d (занято %d, свободно %d)
",
        stat.TotalConns(), stat.MaxConns(), stat.AcquiredConns(), stat.IdleConns())

fmt.Printf("забор коннекта: %d раз, из них с ожиданием: %d
",
        stat.AcquireCount(), stat.EmptyAcquireCount())
```

Stat вызывается в любой момент: это мгновенный снимок состояния пула, без блокировок и без обращения к серверу. Первая строка показывает текущую нагрузку, вторая — дефицит: EmptyAcquireCount растёт, когда горутины ждут свободный коннект. Такой вывод удобно повесить за флагом отладки или отправлять в логи по таймеру — раз в минуту хватает.

> **Не берите коннект вручную без нужды.** pool.Query и pool.Exec сами берут соединение из пула и возвращают его. Вручную Acquire нужен редко: серия запросов на одном коннекте, временные таблицы, advisory-блокировки. Забытый Release после Acquire — тихая дыра: соединение числится занятым, пул открывает новое, MaxConns кончается. Если берёте вручную — Release через defer сразу после Acquire.

### Проверка: дефолт MaxConns

**Вопрос:** Какой MaxConns будет у пула без явной настройки?

- 1
- 10
- ✅ Большее из четырёх и количества ядер CPU
- Сколько успеют создать горутины

Дефолт — большее из 4 и runtime.NumCPU: на восьмиядерной машине — восемь. Значение ниже потолка max_connections базы, но не гарантия: сумму по всем инстансам считает не библиотека, а вы.

### Проверка: лимиты сервера

**Вопрос:** Новый клиент пытается подключиться, а max_connections сервера исчерпаны. Что произойдёт?

- Соединение поставится в очередь и подключится при освобождении слота
- ✅ Подключение отклонится с ошибкой FATAL: sorry, too many clients already
- Соединение установится, но все запросы будут падать
- Postgres автоматически поднимет лимит

Лимит сервера жёсткий: лишнее подключение отклоняется ошибкой ещё на этапе аутентификации. Пул ограничивает MaxConns именно для того, чтобы приложение никогда не стало причиной такой ошибки — очередь живёт внутри пула, а не на сервере.

### Что напечатает программа?

```go
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
    log.Fatal(err)
}

pool.Close()

err = pool.Ping(ctx)
fmt.Println(err)
```

**Что выведет программа?**

- <nil>
- ✅ ошибка: closed pool — пул закрыт
- паника nil pointer
- Ping сам переоткроет пул и вернёт nil

pool.Close() останавливает пул: соединения закрываются, а дальнейшие вызовы возвращают ErrClosedPool. Пул не переоткрывается — это конечный ресурс, а не кэш. Печатается текст ошибки; аккуратный Close ставится в defer и вызывается один раз за жизнь программы.

### Настройте пул из строки подключения

Восстановите создание пула с лимитом соединений и проверкой связи.

```go
pool, err := pgxpool.___(ctx, "postgres://app:secret@localhost:5432/app?___=10")
if err != nil {
        log.Fatal(err)
}
defer pool.___()

if err = pool.___(ctx); err != nil {
        log.Fatal(err)
}
```

Подсказка: Пул создаёт New, лимит задаёт параметр pool_max_conns, закрывает Close, а живость проверяет Ping.

Правильные ответы в пропусках: `New`, `pool_max_conns`, `Close`, `Ping`

### Соберите настройку пула из кода

Расставьте строки в порядке исполнения: разбор конфига, ошибки, лимиты, создание пула, закрытие.

Правильный порядок строк:

1. `cfg, err := pgxpool.ParseConfig(dsn)`
2. `if err != nil {`
3. `  log.Fatal(err)`
4. `}`
5. `cfg.MaxConns = 8`
6. `cfg.MinConns = 2`
7. `pool, err := pgxpool.NewWithConfig(ctx, cfg)`
8. `defer pool.Close()`

### Карточки: пул соединений

- **pgxpool.Pool** — Пул соединений pgx: выдаёт коннекты горутинам, следит за лимитами и жизненным циклом. Создаётся pgxpool.New или NewWithConfig.
- **MaxConns** — Потолок одновременных соединений пула. Дефолт — max(4, NumCPU); сумма по всем инстансам должна быть ниже max_connections.
- **MaxConnLifetime** — Максимальный возраст соединения, дефолт час: старые коннекты закрываются и заменяются свежими.
- **Backend process** — Процесс на стороне Postgres, обслуживающий одно соединение. Соединения дороги: память сервера и лимит max_connections.
- **pool.Stat()** — Снимок состояния пула: TotalConns, AcquiredConns, IdleConns, AcquireCount, EmptyAcquireCount.
- **ErrClosedPool** — Ошибка после pool.Close(): пул не переоткрывается, дальнейшие вызовы возвращают её.

Пул настроен — теперь запросы: в [пятом уроке](/courses/pgx/lessons/05-prostye-zaprosy/) отправим первые INSERT и SELECT и познакомимся с CommandTag. Про мониторинг пула и медленных запросов — [двадцать четвёртый урок](/courses/pgx/lessons/24-nablyudaemost/); про таймауты контекста — [пятнадцатый](/courses/pgx/lessons/15-kontekst-tajmauty/). Формат курса, как и всюду: код с сетью в браузерной песочнице не исполняется — практикуйтесь локально с Go и контейнером Postgres из второго урока, а песочница откроет тематическую программу на чистом Go. Полный план из двадцати пяти уроков живёт на [странице курса pgx](/courses/pgx/) — оттуда видно, куда мы дойдём к финальному проекту.

## Коротко: что запомнить

- Соединение Postgres — процесс на стороне сервера: пул переиспользует коннекты и ограничивает их число.
- pgxpool.New заменяет pgx.Connect одной строкой; API Exec, Query, QueryRow и Begin совпадает.
- Дефолты: MaxConns max(4, NumCPU), MinConns 0, lifetime час, idle тридцать минут, health check минута.
- Считайте сумму MaxConns всех инстансов против max_connections базы — иначе FATAL: too many clients already.
- Ping при старте и в readiness-пробах; таймаут задаётся контекстом.
- pool.Stat() показывает TotalConns, IdleConns и EmptyAcquireCount — меряйте до и после тюнинга.
- Acquire вручную — только для серии запросов на одном коннекте; Release через defer.

## Домашнее задание

Переведите демо из второго урока на pgxpool с pool_max_conns=5 и добавьте пинг при старте. Запустите десять параллельных запросов из десяти горутин и понаблюдайте pool.Stat() до и после: сколько соединений создал пул и сколько раз горутины ждали? Поднимите лимит до двадцати — изменится ли картина? Запишите числа и сравните EmptyAcquireCount при лимитах пять и двадцать: эта разница и есть цена очереди, она пригодится в уроке про наблюдаемость.

## Вопросы и ответы по уроку

### Зачем пул, если можно открывать соединение на каждый запрос?

Каждое соединение — процесс на сервере: сотни параллельных подключений съедят память и лимит max_connections, а хендшейки добавят миллисекунды к каждому запросу. Пул держит несколько готовых коннектов и раздаёт их горутинам: под нагрузкой это предсказуемо и дёшево, а самая дорогая часть — хендшейк и серверный процесс — платится один раз на коннект, а не на запрос.

### Что лучше: MaxConns побольше?

Нет: после пары десятков активных соединений на ядро Postgres тратит время на переключения, а не на запросы. Потолок диктует max_connections базы минус запас, делённые на число инстансов. Больше пользы даст очередь внутри пула, чем толпа коннектов. Если пику действительно нужно больше — масштабируйте базу или ставьте внешний пулер, а не откручивайте лимит до упора.

### Нужен ли pgbouncer рядом с pgxpool?

Когда инстансов сервиса много или соединения открывают serverless-функции — да: pgbouncer мультиплексирует физические соединения. Для одного-трёх сервисов с разумным MaxConns хватает встроенного пула. Классический transaction-режим pgbouncer не дружит с prepared statements — нужен простой протокол запросов или свежая версия pgbouncer с поддержкой protocol-level prepared statements.
