---
title: "Der Startparameter, den PgBouncer abwies: statement_timeout und pgx"
summary: "PgBouncer weist ein Startpaket mit einer Einstellung ab, die es nicht verfolgt, etwa statement_timeout. Ein SET nach dem Verbinden hat es behoben."
author: "Samuel Krauss"
author_title: "Founder"
publisher: "Elchi Studios"
published: 2026-10-03
updated: 2026-10-09
url: https://elchi.dev/de/journal/der-startparameter-den-pgbouncer-abwies
language: de
translation_of: https://elchi.dev/en/journal/the-startup-parameter-pgbouncer-refused
tags: ["postgresql","pgbouncer","go","pgx","connection-pool","statement-timeout"]
words: 971
---

# Der Startparameter, den PgBouncer abwies: statement_timeout und pgx

*Von Samuel Krauss, Founder. Veröffentlicht am 3. Oktober 2026.*

> PgBouncer weist ein Startpaket mit einer Einstellung ab, die es nicht verfolgt, etwa statement_timeout. Ein SET nach dem Verbinden hat es behoben.

Das Backend setzte bei jeder Connection drei PostgreSQL-Einstellungen als Startparameter: die Zeitzone, `statement_timeout` und `idle_in_transaction_session_timeout`. Gegen PostgreSQL funktioniert das. Über PgBouncer, der auf unserem Cluster vor der Datenbank sitzt, wird die Connection vor der ersten Query geschlossen, mit:

```
unsupported startup parameter: statement_timeout
```

Die Lösung ist klein: `application_name` bleibt ein Startparameter, der Rest ist ein `SET` direkt nach dem Verbinden. Warum das beim Session Pooling richtig ist und beim Transaction Pooling falsch wäre, ist der längere Teil.

## Was der Code tat

Mit pgx landen die `RuntimeParams` der Connection-Konfiguration in der Startnachricht:

```go
pcfg.ConnConfig.RuntimeParams["timezone"] = "UTC"
pcfg.ConnConfig.RuntimeParams["statement_timeout"] = "15000"
pcfg.ConnConfig.RuntimeParams["idle_in_transaction_session_timeout"] = "30000"
pcfg.ConnConfig.RuntimeParams["application_name"] = "elchi-web"
```

PostgreSQL behandelt jeden Parameter in der Startnachricht, der nicht zum Protokoll selbst gehört, als Laufzeiteinstellung, die beim Start des Backends angewendet und als Standard der Session behalten wird. Kein Roundtrip, und die Einstellungen gelten vor der ersten Query. Das Muster ist sauber, deshalb ist es verbreitet.

Keines der beiden Timeouts ist Dekoration. 15 Sekunden sind das Maximum, das ein Statement dieser Anwendung dauern darf; ohne diese Grenze hält eine langsame Query einen Request und eine Pool-Connection, so lange sie will. 30 Sekunden darf eine Connection untätig in einer offenen Transaktion verharren, bevor der Server sie beendet, damit eine vergessene Transaktion ihre Locks nicht für immer hält.

## Was PgBouncer mit einem Startpaket macht

PgBouncer liest das Startpaket Parameter für Parameter. `database`, `user` und `options` behandelt es selbst, ebenso `application_name`. Ein Parameter, den es verfolgt, kommt in seinen Cache der Client-Einstellungen. Ein Parameter, der in `ignore_startup_parameters` steht, wird verworfen. Alles andere beendet die Connection mit der Meldung oben.

Was es standardmässig verfolgt, ist laut seiner Dokumentation eine kurze Liste: `application_name`, `client_encoding`, `DateStyle`, `default_transaction_read_only`, `IntervalStyle`, `scram_iterations` (ab PostgreSQL 16), `search_path` (ab PostgreSQL 18), `session_authorization`, `standard_conforming_strings` und `TimeZone`. Das sind Parameter, die PostgreSQL dem Client meldet, wenn sie sich ändern. So kann PgBouncer ihnen folgen und sie auf der Server-Connection wiederherstellen, die der Client als Nächstes bekommt. `statement_timeout` wird nicht gemeldet, also kann PgBouncer ihm nicht folgen und weist es ab, statt so zu tun als ob.

`TimeZone` steht auf der Liste, das wäre also durchgegangen. Welches der beiden Timeouts der Fehler nennt, hängt von der Reihenfolge der Parameter im Paket ab, und pgx füllt das Paket aus einer Go-Map, es kann also jedes der beiden sein.

## Zwei Einstellungen, die wie Lösungen aussehen

PgBouncer hat zwei Optionen, die den Fehler verschwinden lassen. Keine davon wollte ich.

`ignore_startup_parameters = statement_timeout` lässt die Connection durch und verwirft den Parameter. Jedes Statement läuft dann ohne Timeout, und nichts sagt es Ihnen. Das ist schlimmer als der Fehler, der wenigstens den Start stoppt: Unser `Open` pingt die Datenbank, bevor irgendetwas anderes läuft, also scheitert der Prozess, statt Anfragen zu bedienen.

`track_extra_parameters` bittet PgBouncer, einen Wert in seinem Cache zu halten und auf dem Server wiederherzustellen. Die Dokumentation sagt klar, dass sich die meisten Parameter so nicht vollständig verfolgen lassen, denn PgBouncer erfährt von einem `SET` nur bei Parametern, die PostgreSQL meldet. Ausserdem gehört PgBouncer zum Cluster, den sich alle Anwendungen darauf teilen. Eine Anwendung, die nur mit besonderen Pooler-Einstellungen funktioniert, ist eine Sache mehr, an die man bei der nächsten denken muss.

## Die Lösung

Die Einstellungen sind in das `AfterConnect` von pgxpool gewandert, das bei jeder neuen Connection läuft, bevor sie in den Pool kommt:

```go
const sessionSettings = `SET TIME ZONE 'UTC'; SET statement_timeout = 15000; SET idle_in_transaction_session_timeout = 30000`

pcfg.ConnConfig.RuntimeParams["application_name"] = "elchi-web"
pcfg.AfterConnect = func(ctx context.Context, c *pgx.Conn) error {
	_, err := c.Exec(ctx, sessionSettings)
	return err
}
```

pgx schickt eine Query ohne Argumente über das Simple Protocol, das mehrere Statements in einem String erlaubt. Das kostet also einen Roundtrip pro neuer Connection, nicht pro Query. Scheitert es, erreicht die Connection den Pool nie.

`application_name` bleibt ein Startparameter. PgBouncer akzeptiert ihn, und er ist vom ersten Moment an nützlich, in `pg_stat_activity` und in den Logs des Servers. Die Zeitzone ist mit den Timeouts umgezogen, obwohl PgBouncer sie akzeptiert hätte, damit jede Einstellung, auf die sich der Code verlässt, in einem Statement an einer Stelle steht.

## Warum das beim Session Pooling sicher ist

Beim Session Pooling weist PgBouncer einem Client beim Verbinden eine Server-Connection zu und lässt sie zugewiesen, bis der Client die Verbindung trennt. Ein `SET` direkt nach dem Verbinden gilt auf dieser Server-Connection, solange der Client sie hat, also während der Lebensdauer der Pool-Connection. Geht der Client, führt PgBouncer seine `server_reset_query` aus, standardmässig `DISCARD ALL`, bevor die Server-Connection an jemand anderen geht. Unsere 15 Sekunden sickern nicht in die Queries einer anderen Anwendung.

## Warum es beim Transaction Pooling nicht sicher wäre

Beim Transaction Pooling hat ein Client eine Server-Connection nur für eine Transaktion. Das `SET` aus `AfterConnect` würde auf der Server-Connection landen, die dieses eine Statement ausgeführt hat. Diese Connection geht dann zurück in den Pool und weiter an einen anderen Client, mit unseren Timeouts darauf, während unsere nächste Transaktion auf einer Server-Connection ohne sie läuft. Die Reset-Query läuft im Transaction-Modus nicht, ausser `server_reset_query_always` ist eingeschaltet. Die eigene Feature-Tabelle von PgBouncer führt `SET` und `RESET` als Dinge auf, die beim Transaction Pooling nie funktionieren, und Advisory Locks auf Session-Ebene gleich mit.

Nutzen Sie Transaction Pooling, legen Sie die Einstellungen dorthin, wo der Server sie selbst anwendet. `ALTER ROLE ... SET statement_timeout = '15s'` macht den Wert zum Standard für jede neue Server-Session dieser Rolle, egal was dazwischen sitzt, und `SET LOCAL` innerhalb einer Transaktion gilt nur für diese Transaktion. pgx bereitet Statements standardmässig vor, was in diesem Modus ausserdem `max_prepared_statements` von PgBouncer braucht; im Session-Modus braucht es nichts.

Wir nutzen Session Pooling, mit vier Connections pro Instanz, also ist eine Server-Connection pro Pool-Connection ohnehin das, was wir wollen.

## Wie es getestet wurde

`TestSessionSettingsThroughPgBouncer` führt dieselben Prüfungen zweimal aus: einmal direkt gegen die Testdatenbank, einmal über einen echten PgBouncer im Session-Modus davor. Jedes Mal öffnet der Test einen Pool von zwei, nimmt beide Connections gleichzeitig, damit er zwei getrennte Connections prüft und nicht zweimal die erste, und liest auf jeder die Einstellungen zurück:

```sql
SELECT current_setting('TimeZone'), current_setting('statement_timeout'),
       current_setting('idle_in_transaction_session_timeout'), current_setting('application_name')
```

Er erwartet `UTC`, `15s`, `30s` und `elchi-web`. Dann führt er `SELECT pg_sleep(16)` aus und erwartet, dass PostgreSQL es nach 15 Sekunden mit einem Statement Timeout abbricht. Eine Einstellung, die korrekt zurückgelesen wird, aber nichts bewirkt, würde dort scheitern.

Gegen die vorige Version von `Open` scheitert die PgBouncer-Hälfte vor all dem, bei der ersten Connection, mit genau dem Fehler oben. Die direkte Hälfte besteht so oder so, und darin liegt das ganze Problem in einem Test: Eine Testsuite, die nur direkt mit PostgreSQL spricht, kann es nicht sehen.

## Quellen

1. [PgBouncer configuration: ignore_startup_parameters, track_extra_parameters, server_reset_query, pool_mode](https://www.pgbouncer.org/config.html), PgBouncer, gelesen am 2026-10-03
2. [PgBouncer features: pooling modes and what they support](https://www.pgbouncer.org/features.html), PgBouncer, gelesen am 2026-10-03
3. [PgBouncer source: client.c, startup parameter handling](https://raw.githubusercontent.com/pgbouncer/pgbouncer/master/src/client.c), PgBouncer project, gelesen am 2026-10-03
4. [PostgreSQL documentation: Client Connection Defaults (statement_timeout, idle_in_transaction_session_timeout)](https://www.postgresql.org/docs/current/runtime-config-client.html), PostgreSQL Global Development Group, gelesen am 2026-10-03
5. [PostgreSQL documentation: Message Formats, StartupMessage](https://www.postgresql.org/docs/current/protocol-message-formats.html), PostgreSQL Global Development Group, gelesen am 2026-10-03
6. [PostgreSQL documentation: ALTER ROLE](https://www.postgresql.org/docs/current/sql-alterrole.html), PostgreSQL Global Development Group, gelesen am 2026-10-03
7. [pgxpool package documentation (Config.AfterConnect)](https://pkg.go.dev/github.com/jackc/pgx/v5/pgxpool), pkg.go.dev, gelesen am 2026-10-03
