Posted On 13.03.2026

Databases – Postgresql – Gitea and PGBouncer

0 comments
confdroid.com >> blog >> Databases – Postgresql – Gitea and PGBouncer

## Fixing Gitea 500 Errors with PostgreSQL + pgBouncer: The Prepared Statements Trap

If you’ve ever stared at random 500 Internal Server Errors in Gitea — especially on repo views, commit pages, PR lists, or pretty much any action — and your logs are full of cryptic PostgreSQL driver errors like these:

“`text
pq: bind message supplies 3 parameters, but prepared statement “” requires 18
pq: invalid input syntax for type bigint: “confdroid”
pq: bind message supplies 1 parameters, but prepared statement “” requires 2
Repo.GetTopLanguageStats, pq: bind message has 7 result formats but query has 6 columns
“`

…and you’re running Gitea (in my case 1.25.4) behind **pgBouncer** on port 6432 (the classic tell), then congratulations: you’ve hit one of the most frustrating compatibility gotchas in the Postgres ecosystem.

## The Symptoms

– Errors happen intermittently but frequently across almost all UI actions.
– Debug/Trace logging in Gitea shows nothing helpful at first — just stack traces pointing to XORM queries.
– Only Switching to RUN_MODE = **dev** in app.ini finally reveals the real database errors.
– The issue persists even after migrations, schema checks, or fresh databases.
– No obvious resource pressure, connection limits, or timeouts in HAProxy/Kubernetes.

## Root Cause: pgBouncer Transaction Pooling + Server-Side Prepared Statements

Gitea uses XORM (which relies on the Go `lib/pq` driver) and **server-side prepared statements** by default for performance. These statements are prepared once per session and reused.

pgBouncer’s default transaction pooling mode (pool_mode = transaction) reuses the same backend Postgres connection only for the duration of a single transaction. Once the transaction commits/rolls back, the connection goes back to the pool and can be handed to a completely different client. Once the transaction commits/rolls back, the connection goes back to the pool and can be handed to a completely different client.

Prepared statements, however, live at the **session** level — not transaction level. So when pgBouncer gives your Gitea connection a new backend, the previously prepared statement is gone from that backend. The driver tries to bind/execute it anyway → mismatch in **parameter counts, column formats, or even type coercion failures** (like trying to parse a username string as a bigint ID).

Classic symptoms:

– “bind message supplies X parameters, but prepared statement requires Y”
– “prepared statement does not exist”
– Random type errors on perfectly valid data

## Solutions Ranked (What Finally Worked for Me)

1. Best long-term fix: Switch the affected database to session pooling.
pgBouncer allows per-port or per-user overrides. Since my other apps prefer transaction mode for maximum connection efficiency, I added this to my pgbouncer.ini:

“`text
[users]
my_gitea_db_user = pool_mode=session
“`

If you have a number of different connection with different pooling session to manage, you can also configure it per port:

“`text
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
pool_mode = session

[pgbouncer_tx]
listen_addr = 0.0.0.0
listen_port = 6433
pool_mode = transaction

Advertisements

[pgbouncer_stmt]
listen_addr = 0.0.0.0
listen_port = 6434
pool_mode = statement
“`

Reload pgBouncer (`RELOAD`; via admin console or `pgbouncer -R`). Since I am using my own [container solution for pgbouncer](https://sourcecode.confdroid.com/confdroid/confdroid_pgbouncer), I redeployed it to Kubernetes, which restarts it.

**Result**: Prepared statements stay alive for the full Gitea session → no more stale/missing statements. Zero changes needed in Gitea config. Performance impact is minimal for a typical Forge instance (fewer connections overall than you’d think).

2. **Alternative if you can’t touch pool_mode**: Force-disable prepared statements in Gitea
Append `?prepare_threshold=0` to your connection string.
Unfortunately, Gitea builds the DSN from separate fields (`HOST`, `NAME`, `SSL_MODE`), so the trick is:

“`ini
[database]
DB_TYPE = postgres
HOST = :6432
NAME =?prepare_threshold=0&sslmode=require
USER = …
PASSWD = …
“`

This tells `lib/pq` to skip server-side prepare (falling back to simple query + literal params).
But — pgBouncer rejects unknown startup parameters like prepare_threshold → “unsupported startup parameter” error.
→ Only works if you bypass pgBouncer or use direct Postgres connections.

3. **Nuclear option**: Switch to SQLite

If your instance is small/medium and you don’t need Postgres features → just flip to SQLite in app.ini. No pooler, no prepared statement drama. Migration via gitea admin migrate is straightforward. Many production forges run happily on SQLite.

## Key Takeaways

– **Transaction pooling** is great for connection scaling but breaks apps relying on session-scoped features (prepared statements, `SET` commands, temp tables, advisory locks).
– **Session pooling** is safer for ORMs/drivers that prepare statements automatically (Gitea, many Go/Rails/Java apps).
– pgBouncer 1.21+ added experimental prepared statement support in transaction mode (`max_prepared_statements > 0`), but it’s still opt-in and has caveats — test thoroughly if you go that route.
– Always check your pooler port (6432 = pgBouncer default) when seeing weird bind/prepare errors in lib/pq or similar drivers.

After changing to `pool_mode=session` for the Gitea user, all 500s vanished instantly. Pages load, commits render, language stats work — rock solid.
Hope this saves someone else hours (or days) of head-scratching. If you’re running Gitea + Postgres + pgBouncer, double-check that pool mode!
Happy forging!

Did you find this post helpful? You can support me.

[![“Buy Me A Coffee”](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/grizzly_coda)

[![Hetzner Referral](https://confdroid.com/wp-content/uploads/2026/01/hetzner-referral.png)](https://hetzner.cloud/?ref=EY14C8Tema9j)

[![Substack](https://img.shields.io/badge/substack-subscribe_here-green?logo=substack)](https://confdroid.substack.com/subscribe?params=%5Bobject%20Object%5D)

[![ConfDroid Feedback Portal](https://confdroid.com/wp-content/uploads/2026/01/confdroid_feedback_portal.png)](https://feedback.confdroid.com/)

## Related posts
– [Databases – Postgresql – Pilot](https://confdroid.com/db-postgres-pilot/)
– [Databases – Postgresql – PGbouncer](https://confdroid.com/db-postgres-pgbouncer/)

Author Profile

12ww1160DevOps engineer & architect

Advertisements

Leave a Reply

Your email address will not be published. Required fields are marked *

6 + 10 =

Related Post

Puppet with Foreman – Installation

## Getting Started: Installing Your First Puppet Server with Foreman in Under 30 Minutes In…

Grafana with Keycloak – editing dashboards

## Streamlining Authentication: Integrating Keycloak with Grafana via OIDC In today's complex IT landscapes, managing…

Kubernetes – What’s all the fuzz about it?

So I have been in the IT business for more than 20 years and seen…
Social Media Auto Publish Powered By : XYZScripts.com