cnx / laravel-pgbouncer-reconnect
Resets pgBouncer server connections after migrations that change the database schema
Package info
github.com/cnxapp/laravel-pgbouncer-reconnect
pkg:composer/cnx/laravel-pgbouncer-reconnect
Requires (Dev)
- illuminate/database: ^10.0 || ^11.0 || ^12.0
- illuminate/support: ^10.0 || ^11.0 || ^12.0
- phpunit/phpunit: ^10.0 || ^11.0
Suggests
- illuminate/support: Registers the migration listener automatically in Laravel
README
Resets pgBouncer server connections after migrations that change the database schema.
The problem
PostgreSQL caches query plans together with the shape of their result — how many columns and of which types. ALTER TABLE ... ADD COLUMN changes that shape, which makes every cached plan for the table invalid:
SQLSTATE[0A000] ERROR: cached plan must not change result type
Normally this resolves itself: the connection is re-established and the plan is rebuilt. Behind pgBouncer it does not. Plans live on the pooled server connections, which outlive application restarts, and server_lifetime only recycles a connection once it goes idle — under load it never does.
The result is an application failing every query against the changed tables long after the migration succeeded, until the pool is reset by hand.
This package resets it automatically: once migrations have run, it issues RECONNECT on the pgBouncer admin console.
Installation
composer require cnx/laravel-pgbouncer-reconnect
The service provider is registered through package discovery — no application code changes are needed.
Publishing the config is optional:
php artisan vendor:publish --tag=pgbouncer-reconnect-config
How it works
- Listens for
MigrationsEndedand acts only when migrations were actually applied. A deploy without migrations leaves live connections untouched. - Connects to the
pgbounceradmin database on the same host and port as the application, reusing the same credentials. No extra secret is required as long as the application role is listed inadmin_users. RECONNECTdoes not drop client connections: pgBouncer closes each server connection as it becomes idle, i.e. after the current transaction finishes.
Environments without pgBouncer
Detected automatically — nothing to configure. A plain PostgreSQL server has no pgbouncer database, so the connection fails immediately, a debug message is recorded and execution continues.
This is the normal path for environments that talk to PostgreSQL directly, for local development and for tests.
Configuration
| Variable | Default | Purpose |
|---|---|---|
PGBOUNCER_RECONNECT_ENABLED |
true |
Disable the behaviour explicitly |
PGBOUNCER_RECONNECT_CONNECTION |
default connection | Name of a connection from database.connections |
pgBouncer requirements
The role your application connects with must be listed in admin_users:
admin_users = my_app_role
Otherwise RECONNECT is rejected for lack of privileges — the package logs a warning and carries on without failing the deploy.
Design notes
Never throws. The reset runs after migrations have already been applied. A failed reset is a warning, not a reason to turn a successful migration into a failed deploy.
PDO::exec(), never prepare(). The pgBouncer admin console does not speak the extended query protocol:
extended query protocol not supported by admin console
That rules out DB::statement(), which prepares its queries. A dedicated PDO connection using exec() is required. Verified against pgBouncer 1.23 and 1.25.
The core is framework-agnostic. PoolReset takes a connection config array and a PSR-3 logger, so it is testable without booting a framework. Laravel integration lives entirely in the service provider.
Compatibility
- PHP 8.1+
- Laravel 10, 11, 12
- PostgreSQL behind pgBouncer in any pool mode
Tests
composer install vendor/bin/phpunit
License
Proprietary.