Skip to main content

db-retry

PyPI version Supported Python versions Downloads Coverage CI License GitHub stars Context7 uv Ruff ty

Retry helpers for PostgreSQL and SQLAlchemy, plus multi-host connection and transaction utilities.

Features

  • postgres_retry: retries a coroutine on serialization failures and lost connections.
  • build_connection_factory: multi-host asyncpg connections with failover on timeout.
  • build_db_dsn and is_dsn_multihost: DSN parsing and manipulation.
  • Transaction: transaction context manager that rolls back and closes the session on exit.

Installation

Using uv

uv add db-retry

Using pip

pip install db-retry

Usage examples

1. Database operations with automatic retry

Retry a coroutine on serialization failures and lost connections. Either error ends the whole Postgres transaction, so the retried coroutine has to own its transaction from start to finish: open it with Transaction, as below, or work on a fresh session it creates itself. A coroutine that runs on a session someone else began cannot be retried. Postgres has already discarded the statements that ran before it, and SQLAlchemy refuses the next attempt with PendingRollbackError instead of committing part of the work. One case is recovered for you: if the connection is lost on a query or flush that runs after the Transaction block, entering Transaction on the next attempt rolls back the dead transaction and starts on a fresh connection.

import asyncio
import sqlalchemy as sa
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from db_retry import Transaction, postgres_retry


class Base(DeclarativeBase):
    pass


class User(Base):
    __tablename__ = "users"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(sa.String())
    email: Mapped[str] = mapped_column(sa.String(), index=True)


# Total attempts come from DB_RETRY_RETRIES_NUMBER (default 3: the first call plus 2 retries)
@postgres_retry
async def get_user_by_email(session: AsyncSession, email: str) -> User | None:
    async with Transaction(session=session):
        return await session.scalar(sa.select(User).where(User.email == email))


async def main():
    engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/mydb")
    async with AsyncSession(engine) as session:
        # Automatically retries on connection failures or serialization errors
        user = await get_user_by_email(session, "john.doe@example.com")
        if user:
            print(f"Found user: {user.name}")


asyncio.run(main())

Per-callsite override of the total number of attempts:

@postgres_retry(retries=5)
async def create_order(session: AsyncSession, order: Order) -> Order: ...

2. High availability database connections

Connect to one of several hosts:

from sqlalchemy.ext.asyncio import create_async_engine
from db_retry import build_connection_factory, build_db_dsn

# Configure multiple database hosts for high availability
multi_host_dsn = "postgresql://user:password@/myapp_db?host=primary-db:5432&host=secondary-db:5432&host=backup-db:5432"

# Set the database name, driver and target_session_attrs
dsn = build_db_dsn(db_dsn=multi_host_dsn, database_name="production_database", drivername="postgresql+asyncpg")

# Create connection factory with timeout
connection_factory = build_connection_factory(
    url=dsn,
    timeout=5.0,  # 5 second connection timeout
)

# On a connect timeout, the factory tries each host in turn
engine = create_async_engine(dsn, async_creator=connection_factory)

3. Transaction management

Wrap a unit of work in a transaction that is rolled back and closed on exit:

import dataclasses
import datetime
import typing

from schemas import AnalyticsEventCreate, AnalyticsEvent
from db_retry import Transaction, postgres_retry

from your_service_name.database.tables import EventsTable
from your_service_name.producers.analytics_service_events_producer import AnalyticsEventsProducer
from your_service_name.repositories.events_repository import EventsRepository
from your_service_name.settings import settings


@dataclasses.dataclass(kw_only=True, frozen=True, slots=True)
class CreateEventUseCase:
    events_repository: EventsRepository
    transaction: Transaction
    analytics_events_producer: AnalyticsEventsProducer

    @postgres_retry
    async def __call__(
        self,
        event_create_data: AnalyticsEventCreate,
    ) -> AnalyticsEvent:
        async with self.transaction:
            model: typing.Final = EventsTable(
                **event_create_data.model_dump(),
                created_at=datetime.datetime.now(tz=settings.common.default_timezone),
            )
            saved_event: typing.Final[EventsTable] = await self.events_repository.create(model)
            event: typing.Final = AnalyticsEvent.model_validate(saved_event)
            await self.analytics_events_producer.send_message(event)
            await self.transaction.commit()
            return event

These projects use this use-case shape in full, with pool_pre_ping off and postgres_retry handling connections that died while idle in the pool:

4. Serializable transactions

Use the serializable isolation level to prevent race conditions:

from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from db_retry import Transaction


async def main():
    engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/mydb")

    async with AsyncSession(engine) as session:
        strict_transaction = Transaction(
            session=session,
            isolation_level="SERIALIZABLE",
        )
        # use strict_transaction where needed

Configuration

The library can be configured using environment variables:

Variable Description Default
DB_RETRY_RETRIES_NUMBER Total attempts (first call plus retries) 3

Example:

export DB_RETRY_RETRIES_NUMBER=5

API reference

Retry decorator

  • @postgres_retry - Decorator for async functions that should retry on database errors (total attempts, first call plus retries, from DB_RETRY_RETRIES_NUMBER)
  • @postgres_retry(retries=N) - Override the total attempts (first call plus retries) per callsite
  • Retries a serialization failure (40001), a connection error (class 08), and any DBAPIError that SQLAlchemy marks with connection_invalidated. The last one covers asyncpg's InterfaceError: connection is closed on a pooled connection that died while idle, which is what pool_pre_ping=True otherwise catches
  • The retried coroutine must own its whole transaction, through Transaction or a fresh session; on a session another caller began, the next attempt raises PendingRollbackError
  • Retries also fire when the retriable asyncpg error is wrapped by advanced-alchemy's wrap_sqlalchemy_exception() (i.e. surfaced as RepositoryError / IntegrityError); the handler walks the __cause__ / __context__ chain.

Connection utilities

  • build_connection_factory(url, timeout) - Creates a connection factory for multi-host setups
  • build_db_dsn(db_dsn, database_name, use_replica=False, drivername="postgresql") - Builds a DSN with the given database name and driver, and sets target_session_attrs to read-write, or to prefer-standby when use_replica=True
  • is_dsn_multihost(db_dsn) - Checks if a DSN contains multiple hosts

Transaction helper

  • Transaction(session, isolation_level=None) - Context manager for transaction handling; on entry, rolls back a transaction that can no longer continue (a failed flush or a lost connection), otherwise joins the session's open transaction or begins one; on exit, rolls back if still in a transaction, then closes the session

Requirements

  • Python 3.11+
  • SQLAlchemy with asyncio support
  • asyncpg PostgreSQL driver

Part of modern-python

Browse the full list of templates and libraries in modern-python; see the org profile for the categorized index.

Metadata

Release files for db-retry 0.8.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for db-retry 0.8.2
File Size Uploaded Provenance
db_retry-0.8.2.tar.gz 9.2 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for db-retry 0.8.2
File Interpreter ABI Platform
db_retry-0.8.2-py3-none-any.whl Python 3 none any Details

Total release size: 18.9 kB

Release files / db_retry-0.8.2.tar.gz

Download URL db_retry-0.8.2.tar.gz
Size 9.2 kB
Tags Source
SHA-256 checksum
How to use checksums
dc71270f3e97cc84c0bf25fee20fe6bacee14045557a254a156a1cd02b3b3c1f
BLAKE2b-256 checksum
How to use checksums
3401b743dc32f4ecc7a29e363267321e9d5182e132fe6a08efbadef16e1945aa
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.4 {"installer":{"name":"uv","version":"0.13.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / db_retry-0.8.2-py3-none-any.whl

Download URL db_retry-0.8.2-py3-none-any.whl
Size 9.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
124119f6d0c2aed8909c56c3c40f95a78f1a6ab5b289b16a4e82837a4ca61e44
BLAKE2b-256 checksum
How to use checksums
46d4f0e25de52e0ca07ab27e9aac512087d98dab6e821fc54baf53129c857ab4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.4 {"installer":{"name":"uv","version":"0.13.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for db-retry 0.8.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page