Describe the bug
When Bazarr is configured to use PostgreSQL, connections leak steadily over time from scheduled/background jobs (e.g. the periodic Sonarr/Radarr sync), independent of the leak already fixed in #3361 (which was specific to the /api/system/status endpoint). Left unattended, this eventually exhausts the PostgreSQL role's connection limit and Bazarr starts failing with:
(psycopg2.OperationalError) connection to server at "postgres" ... failed: FATAL: sorry, too many clients already
In my case this also caused a shared-Postgres-instance side effect: Jellyseerr, which uses the same Postgres server for its own database, was unable to log users in because no connections were available for it either.
Root cause (from reading app/database.py and app/app.py on main)
# app/database.py
session_factory = sessionmaker(bind=engine)
database = scoped_session(session_factory) # thread-local session
# app/app.py
@app.before_request
def _db_connect():
database.begin()
@app.teardown_request
def _db_close(exc):
database.close()
scoped_session (default scope: current thread) hands each thread its own persistent Session, which holds one checked-out PostgreSQL connection open until database.close()/database.remove() is called for that thread. That cleanup is wired only to Flask's before_request/teardown_request hooks, i.e. it only runs for HTTP/API requests.
app/scheduler.py's background jobs (APScheduler) run independently of Flask's request cycle — I don't see any app_context()/request_context() wrapping around scheduled job execution. So whenever a scheduled job (e.g. the periodic library sync) touches the database on a scheduler worker thread, it opens a Postgres connection via scoped_session that is never closed. The only other place database.remove() is called is an atexit hook, which only fires on process shutdown — not after each job.
The NullPool used for the Postgres engine (create_engine(url, poolclass=NullPool, isolation_level="AUTOCOMMIT")) doesn't mitigate this: NullPool means SQLAlchemy doesn't pool at all, so a connection is only actually released back to Postgres when the session is explicitly closed — which for scheduler jobs, never happens.
To Reproduce
- Configure Bazarr to use PostgreSQL with a Postgres role/database that has a modest connection limit.
- Leave Bazarr running with its default scheduled jobs enabled (hourly sync, etc.) and do not touch the web UI/API at all.
- Watch
pg_stat_activity for the Bazarr role over several hours/days — a new idle connection accumulates roughly in step with each scheduled job run, and is never released.
- Eventually the role hits its connection limit and scheduled jobs start failing with "too many clients"/"too many connections for role" errors.
Expected behavior
Connections opened by scheduled/background jobs should be returned/closed when the job finishes, the same way teardown_request does for HTTP requests — e.g. by wrapping job execution so it also calls database.close()/database.remove() on completion (or by disposing engine-per-job / using an app context around scheduled job execution).
Software
- Bazarr: v1.5.6 (
linuxserver/bazarr:latest, built 2026-05-12)
- Database: PostgreSQL 16
- Also reproduced by inspecting the current
app/database.py / app/app.py on main — the thread-scoped-session-without-per-job-teardown pattern is unchanged, so this looks like it's still present.
Additional context
This is distinct from #3361, which fixed a different leak (a raw engine connection opened per /api/system/status poll). That fix (caching the DB/migration version at init_db() startup) doesn't touch the before_request/teardown_request mechanism, and doesn't affect the scheduler-thread leak described here.
As a workaround, I've set idle_session_timeout on the Postgres server so it force-closes long-idle leaked connections, which prevents exhaustion but obviously doesn't fix the underlying leak.
Describe the bug
When Bazarr is configured to use PostgreSQL, connections leak steadily over time from scheduled/background jobs (e.g. the periodic Sonarr/Radarr sync), independent of the leak already fixed in #3361 (which was specific to the
/api/system/statusendpoint). Left unattended, this eventually exhausts the PostgreSQL role's connection limit and Bazarr starts failing with:In my case this also caused a shared-Postgres-instance side effect: Jellyseerr, which uses the same Postgres server for its own database, was unable to log users in because no connections were available for it either.
Root cause (from reading
app/database.pyandapp/app.pyonmain)scoped_session(default scope: current thread) hands each thread its own persistentSession, which holds one checked-out PostgreSQL connection open untildatabase.close()/database.remove()is called for that thread. That cleanup is wired only to Flask'sbefore_request/teardown_requesthooks, i.e. it only runs for HTTP/API requests.app/scheduler.py's background jobs (APScheduler) run independently of Flask's request cycle — I don't see anyapp_context()/request_context()wrapping around scheduled job execution. So whenever a scheduled job (e.g. the periodic library sync) touches the database on a scheduler worker thread, it opens a Postgres connection viascoped_sessionthat is never closed. The only other placedatabase.remove()is called is anatexithook, which only fires on process shutdown — not after each job.The
NullPoolused for the Postgres engine (create_engine(url, poolclass=NullPool, isolation_level="AUTOCOMMIT")) doesn't mitigate this: NullPool means SQLAlchemy doesn't pool at all, so a connection is only actually released back to Postgres when the session is explicitly closed — which for scheduler jobs, never happens.To Reproduce
pg_stat_activityfor the Bazarr role over several hours/days — a new idle connection accumulates roughly in step with each scheduled job run, and is never released.Expected behavior
Connections opened by scheduled/background jobs should be returned/closed when the job finishes, the same way
teardown_requestdoes for HTTP requests — e.g. by wrapping job execution so it also callsdatabase.close()/database.remove()on completion (or by disposing engine-per-job / using an app context around scheduled job execution).Software
linuxserver/bazarr:latest, built 2026-05-12)app/database.py/app/app.pyonmain— the thread-scoped-session-without-per-job-teardown pattern is unchanged, so this looks like it's still present.Additional context
This is distinct from #3361, which fixed a different leak (a raw engine connection opened per
/api/system/statuspoll). That fix (caching the DB/migration version atinit_db()startup) doesn't touch thebefore_request/teardown_requestmechanism, and doesn't affect the scheduler-thread leak described here.As a workaround, I've set
idle_session_timeouton the Postgres server so it force-closes long-idle leaked connections, which prevents exhaustion but obviously doesn't fix the underlying leak.