<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: krylosov-aa</title>
    <description>The latest articles on DEV Community by krylosov-aa (@krylosov-aa).</description>
    <link>https://dev.to/krylosov-aa</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3653345%2Fb66e8f92-2528-4f2e-91df-8698b6cc4bcf.jpeg</url>
      <title>DEV Community: krylosov-aa</title>
      <link>https://dev.to/krylosov-aa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/krylosov-aa"/>
    <language>en</language>
    <item>
      <title>Three PostgreSQL Master/Replica Discovery Problems — and How to Solve Them</title>
      <dc:creator>krylosov-aa</dc:creator>
      <pubDate>Sun, 14 Jun 2026 17:16:55 +0000</pubDate>
      <link>https://dev.to/krylosov-aa/three-postgresql-masterreplica-discovery-problems-and-how-to-solve-them-4c8h</link>
      <guid>https://dev.to/krylosov-aa/three-postgresql-masterreplica-discovery-problems-and-how-to-solve-them-4c8h</guid>
      <description>&lt;p&gt;When an application starts using multiple PostgreSQL hosts, the headaches begin: you need to dynamically find the master after a failover, pick a replica with an acceptable replication lag, and guarantee that a user won't see stale data immediately after their own write. DNS caches for minutes, libpq knows nothing about lag, HAProxy has never heard of LSN. Let's look at how existing solutions work and how to cover all three problems with a lightweight HTTP service — &lt;a href="https://github.com/krylosov-aa/pg-status" rel="noopener noreferrer"&gt;pg-status&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  PostgreSQL Master and Replicas
&lt;/h2&gt;

&lt;p&gt;There comes a point in an application's life when a single PostgreSQL host is no longer enough. I'll highlight the two main reasons:&lt;/p&gt;

&lt;h3&gt;
  
  
  Resilience
&lt;/h3&gt;

&lt;p&gt;A single host can go down or become unreachable on the network. If the application has only one PostgreSQL host, this can mean a complete outage. To survive the failure of any individual host, teams set up multiple hosts in a master-replica scheme.&lt;/p&gt;

&lt;p&gt;The scheme works like this: there is one master host that accepts write requests from users. There are one or more replica hosts that accept only read requests. Data reaches the replica through &lt;em&gt;streaming replication&lt;/em&gt;: the replica connects to the master and continuously replays its WAL journal (Write-Ahead Log) — the log of all data changes that PostgreSQL maintains on the master.&lt;/p&gt;

&lt;p&gt;This immediately implies an important property: a replica is &lt;strong&gt;always slightly behind&lt;/strong&gt; the master. There is a delay between a transaction being committed on the master and it being replayed on the replica — this is called &lt;em&gt;replication lag&lt;/em&gt;. It is usually milliseconds, but under heavy load or network issues it can grow to seconds or more. This is normal and expected — which is exactly why it matters to know how far behind a replica is before reading from it.&lt;/p&gt;

&lt;p&gt;In this scheme, if the master becomes unavailable, one of the replicas can be promoted to the new master, and the application survives the incident. This is called a &lt;em&gt;failover&lt;/em&gt;. Importantly, failover does not happen automatically on its own — it must be orchestrated. Tools exist for this purpose that monitor cluster state and, when the master goes down, automatically elect a new one from among the replicas and reconfigure replication. Without such a tool, the switch has to be done manually. Even without failover, if the application reads from replicas, read-only functionality can continue to be served while the master is unavailable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Throughput
&lt;/h3&gt;

&lt;p&gt;An application can become so loaded that the PostgreSQL host becomes the bottleneck. PostgreSQL is not a distributed database, and it is not easy to scale horizontally. But in most applications, 80–90% of database queries are reads. Routing them to replicas offloads the master in exactly the places that hurt: CPU for query processing and I/O for reading data from disk. The master is left to handle only writes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problems on the Application Side
&lt;/h2&gt;

&lt;p&gt;Standing up multiple hosts is only half the battle. The headaches start on the application side, which previously made queries to a single static host. Now the right host must be found dynamically, since a failover with a master switch can happen at any moment, and replicas each have different lag.&lt;/p&gt;

&lt;p&gt;Three concrete problems arise:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Find the current live master.&lt;/strong&gt; Any application needs this — you can't write without a master. You can't simply hardcode the master address in a config: after a failover a different host becomes the master, and the application must learn this without restarting. So the master address needs to be determined dynamically — ideally before every write session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Find a sufficiently in-sync replica.&lt;/strong&gt; This is needed to offload the master. There may be several replicas, each with different lag — you need to pick a suitable one. Lag is measured in two dimensions: by time (how many milliseconds the replica is behind the master) and by WAL bytes (how much unreplayed data has accumulated). What "sufficiently" means depends on context — a 5-second lag is acceptable for an analytics report, but not for a user profile page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Guarantee read-your-writes.&lt;/strong&gt; The user saved data, the page reloaded and... showed old data. This happens because the write went to the master, but the next read went to a replica that hadn't caught up yet. This is a stricter version of problem 2: instead of a threshold lag, you need a point guarantee — the replica must have replayed data up to the exact WAL position that a specific transaction left behind.&lt;/p&gt;

&lt;p&gt;In this article I'll explain how to solve all three problems on the application side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Existing Solutions and Their Limitations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  libpq Multi-Host
&lt;/h3&gt;

&lt;p&gt;In libpq, you can list hosts directly in the connection string and specify the desired host type via &lt;code&gt;target_session_attrs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;host&lt;/span&gt;=&lt;span class="n"&gt;host&lt;/span&gt;-&lt;span class="m"&gt;1&lt;/span&gt;,&lt;span class="n"&gt;host&lt;/span&gt;-&lt;span class="m"&gt;2&lt;/span&gt;,&lt;span class="n"&gt;host&lt;/span&gt;-&lt;span class="m"&gt;3&lt;/span&gt; &lt;span class="n"&gt;target_session_attrs&lt;/span&gt;=&lt;span class="n"&gt;read&lt;/span&gt;-&lt;span class="n"&gt;write&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;libpq tries hosts sequentially and picks a suitable one — all built into the driver.&lt;/p&gt;

&lt;p&gt;Values for &lt;code&gt;target_session_attrs&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;read-write&lt;/code&gt; — master only&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;read-only&lt;/code&gt; — replica only&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;prefer-standby&lt;/code&gt; — replica if available, otherwise master&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;any&lt;/code&gt; — any live host&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sounds convenient, but there are significant limitations on closer inspection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Finding the right host requires real TCP connections.&lt;/strong&gt; libpq determines host type by running &lt;code&gt;SELECT pg_is_in_recovery()&lt;/code&gt; after establishing a connection. If the first host is a replica but a master is needed, libpq connects to it, runs the query, discovers it's a replica, tears down the connection, and moves on to the next. Each such attempt is a full TCP handshake plus PostgreSQL authentication. With three hosts, in the worst case the right one isn't reached until the third attempt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slow failover detection.&lt;/strong&gt; libpq only learns about a master change when the current connection breaks or a query returns an error. While the connection to the old master is alive, libpq keeps writing to it — even if that host has already transitioned to replica mode. There is no background monitoring that signals "this host changed roles."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No lag control.&lt;/strong&gt; &lt;code&gt;prefer-standby&lt;/code&gt; returns any live replica, even one that's 30 minutes behind. There's no way to say "return a replica with lag_ms ≤ 100."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No read-your-writes.&lt;/strong&gt; No built-in mechanism.&lt;/p&gt;

&lt;h3&gt;
  
  
  DNS
&lt;/h3&gt;

&lt;p&gt;A simple approach: a DNS record always points to the current master, and when a failover happens the DNS record is updated. Minimal infrastructure. But updating DNS is not an instantaneous operation, and in practice the delay can be significantly longer than it seems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Existing connections will not switch at all.&lt;/strong&gt; This is the most important point. DNS is resolved once when a connection is established. If you have a pool of 20 connections to the old master — they will stay on it until the connections are recreated. No DNS update will switch them. Switching only happens when the pool is forced to create a new connection — on a reuse timeout, on an error, or manually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-layer caching.&lt;/strong&gt; Even for new connections, a DNS record passes through several independent caches: the OS resolver (30–60 s), JVM (forever by default — &lt;code&gt;networkaddress.cache.ttl = -1&lt;/code&gt;), Kubernetes CoreDNS (30 s), and the connection pool which recreates connections on its own schedule. The delays add up, and after a failover the new address reaches the application after tens of seconds to minutes.&lt;/p&gt;

&lt;p&gt;So during a failover, a master switch through DNS can take anywhere from tens of seconds to several minutes — depending on the stack. And even after the DNS change, some traffic will still hit the old host through existing connections in the pool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No metadata.&lt;/strong&gt; DNS returns only an address, nothing about replica lag.&lt;/p&gt;

&lt;h3&gt;
  
  
  HAProxy
&lt;/h3&gt;

&lt;p&gt;HAProxy is a TCP/HTTP load balancer that can health-check backends and route connections. For PostgreSQL the typical setup has two listeners — one for writes (master only), one for reads (replicas). The application itself decides which port to connect to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The built-in health check doesn't detect host role.&lt;/strong&gt; HAProxy can verify backend reachability at the network level, but it can't distinguish master from replica with built-in tools. Role detection requires an external HTTP endpoint on each PostgreSQL host that returns 200 or 503 depending on role. You have to implement and maintain this endpoint yourself — either via an auxiliary process or via an existing cluster orchestration tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lag control is entirely manual work.&lt;/strong&gt; HAProxy knows nothing about PostgreSQL replication. To exclude lagging replicas, you have to implement the logic in the health-check endpoint yourself: query the WAL position or timestamp of the last replayed event, calculate the lag, and return 503 if a threshold is exceeded. This is extra code to write, test, and maintain. The lag threshold is yet another configuration constant with no way to override it per request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HAProxy sits in the path of SQL queries.&lt;/strong&gt; Every connection to the database passes through it — an extra network hop. For HA you need a second HAProxy with keepalived and a VIP, otherwise HAProxy itself becomes a single point of failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read-your-writes is impossible in principle.&lt;/strong&gt; HAProxy operates at the TCP connection level and has no access to the SQL query contents — it doesn't know what LSN a client's transaction left behind.&lt;/p&gt;

&lt;h3&gt;
  
  
  pgpool-II
&lt;/h3&gt;

&lt;p&gt;A full SQL proxy: parses every query, decides where to route it (SELECT → replica, INSERT/UPDATE → master), pools client connections, manages failover. Transparent to the application — no code changes needed.&lt;/p&gt;

&lt;p&gt;Compared to simpler solutions, pgpool-II has real lag control support:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;delay_threshold&lt;/code&gt; — maximum replica lag in WAL bytes. If a replica exceeds the threshold, pgpool stops sending SELECTs to it until it catches up.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;delay_threshold_by_time&lt;/code&gt; (pgpool-II 4.4+) — same, but in time units (milliseconds by default), using &lt;code&gt;pg_stat_replication.replay_lag&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;prefer_lower_delay_standby&lt;/code&gt; (pgpool-II 4.3+) — if the selected replica exceeds the threshold, pgpool picks the least-lagging replica instead of falling back to master.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is noticeably better than DNS or libpq. But there are important limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sits in the path of SQL queries.&lt;/strong&gt; Every query to the database goes through pgpool — an extra network hop. pgpool itself becomes a single point of failure; HA mode requires a watchdog setup with a VIP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load balancing is chosen at session level, not query level.&lt;/strong&gt; The replica host is assigned when the connection is established and doesn't change until it's closed — unless &lt;code&gt;statement_level_load_balance&lt;/code&gt; is enabled. And inside an explicit transaction, after the first write query, all subsequent SELECTs go to the master until the transaction ends.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lag thresholds are global.&lt;/strong&gt; &lt;code&gt;delay_threshold&lt;/code&gt; and &lt;code&gt;delay_threshold_by_time&lt;/code&gt; are configuration parameters, not query parameters. You can't say "apply a 50 ms threshold for this specific query." One threshold for the entire application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No read-your-writes via LSN.&lt;/strong&gt; pgpool-II provides no mechanism for passing a specific WAL position from the client.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Patroni REST API
&lt;/h3&gt;

&lt;p&gt;If you're already using Patroni for PostgreSQL cluster management, it has an HTTP API: &lt;code&gt;/leader&lt;/code&gt;, &lt;code&gt;/primary&lt;/code&gt;, &lt;code&gt;/replica&lt;/code&gt;, &lt;code&gt;/health&lt;/code&gt;, &lt;code&gt;/cluster&lt;/code&gt;, and others.&lt;/p&gt;

&lt;p&gt;Patroni can do more than it might seem. The &lt;code&gt;/replica?lag=10MB&lt;/code&gt; endpoint returns HTTP 200 only if the replica is no more than 10 MB behind — you can specify in bytes or human-readable format (&lt;code&gt;16kB&lt;/code&gt;, &lt;code&gt;64MB&lt;/code&gt;, &lt;code&gt;1GB&lt;/code&gt;). The &lt;code&gt;/cluster&lt;/code&gt; endpoint returns complete information about each cluster member, including &lt;code&gt;replay_lsn&lt;/code&gt;, &lt;code&gt;replay_lag&lt;/code&gt;, &lt;code&gt;receive_lsn&lt;/code&gt;, &lt;code&gt;receive_lag&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;This is a health-check API, not a discovery API.&lt;/strong&gt; &lt;code&gt;/replica?lag=X&lt;/code&gt; returns HTTP 200 or 503 — "this node is suitable or not." It doesn't return a hostname to connect to. This format is designed for use with a load balancer (like HAProxy) that is already pointed at a specific node and verifies its suitability. To independently select a suitable replica, you have to poll each node separately or parse the &lt;code&gt;/cluster&lt;/code&gt; response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Patroni is a full-featured HA cluster manager.&lt;/strong&gt; It requires an external DCS (etcd, Consul, or ZooKeeper). If Patroni is already in place — use its API. If not — deploying the whole stack just for discovery is overkill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No read-your-writes via LSN.&lt;/strong&gt; &lt;code&gt;/replica?lag=X&lt;/code&gt; filters by threshold lag in bytes, but there's no way to pass a specific WAL position and get a replica that has already replayed data up to that position.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  pg-status — an HTTP Discovery Service
&lt;/h3&gt;

&lt;p&gt;I built a microservice &lt;a href="https://github.com/krylosov-aa/pg-status" rel="noopener noreferrer"&gt;pg-status&lt;/a&gt; — a lightweight HTTP service that polls PostgreSQL hosts in the background and answers questions like "who is the current master?", "which replica is within 100 ms of lag?", "which replica has already replayed a specific WAL LSN?" — all from memory, in fractions of a millisecond.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does not sit in the path of SQL queries.&lt;/strong&gt; This is the fundamental architectural difference from HAProxy and pgpool-II. The application connects to PostgreSQL directly — pg-status only advises &lt;em&gt;which host&lt;/em&gt; to connect to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Active background monitoring, not reaction to errors.&lt;/strong&gt; Unlike libpq and DNS, pg-status doesn't wait for a connection to fail. It continuously polls all hosts in the background and always has an up-to-date picture. A master change is detected within one polling interval (5 seconds by default, configurable) — not after tens of seconds of TTL, not after a connection drops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lag control per request or global config.&lt;/strong&gt; A lag threshold can be set globally, or passed directly as a query parameter. One application endpoint can require lag ≤ 50 ms while another is fine with 5 seconds — with no configuration changes and no service restart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read-your-writes via LSN.&lt;/strong&gt; The only solution reviewed here that provides a built-in tool for this pattern. After a write to the master, the application passes the transaction's WAL position in a request to pg-status — and gets back only the replica that has already replayed data up to that position (or the master).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automatic load balancing across replicas.&lt;/strong&gt; When multiple replicas fall within the acceptable lag, pg-status automatically distributes load among them round-robin. No need to implement balancing yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack and platform agnostic.&lt;/strong&gt; Plain HTTP, response in plain text or JSON. Can be called from any programming language with a single HTTP request, no special client libraries needed. Written in C, supports Linux and macOS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Minimal infrastructure requirements.&lt;/strong&gt; No etcd, Consul, or ZooKeeper like Patroni. No watchdog cluster like pgpool-II in HA mode. One binary, a few environment variables — and it works. Consumes 9 MB of RAM and responds fast enough to call before every database query. Can be deployed as a sidecar so each application instance has an independent discovery helper, or deployed as one or more global instances for the whole application.&lt;/p&gt;

&lt;p&gt;Let's walk through how to use pg-status in practice — starting with each of the three problems individually, then a full Python integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solving Problem 1: Finding the Master
&lt;/h2&gt;

&lt;p&gt;pg-status provides the &lt;code&gt;GET /master&lt;/code&gt; endpoint for this, which returns the hostname of the current master:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl http://localhost:8000/master
host-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application calls this before every write session. pg-status continuously polls hosts in the background and tracks master changes itself — your code simply asks "who is the master right now?" and gets a current answer.&lt;/p&gt;

&lt;p&gt;Adding the &lt;code&gt;Accept: application/json&lt;/code&gt; header returns the response as JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept: application/json"&lt;/span&gt; http://localhost:8000/master
&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"host"&lt;/span&gt;: &lt;span class="s2"&gt;"host-1"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If there is no master — a 404 is returned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solving Problem 2: Finding a Synchronized Replica
&lt;/h2&gt;

&lt;p&gt;Here pg-status provides several endpoints depending on the required guarantee. The selection logic is straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Just any live replica with no freshness requirements — &lt;code&gt;/replica&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Time lag matters (milliseconds) — &lt;code&gt;/sync_by_time&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Volume of unapplied WAL matters (bytes) — &lt;code&gt;/sync_by_bytes&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Either condition is acceptable — &lt;code&gt;/sync_by_time_or_bytes&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Both conditions must hold simultaneously — &lt;code&gt;/sync_by_time_and_bytes&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The most up-to-date replica, deterministically — &lt;code&gt;/most_sync_by_bytes&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Just a live replica&lt;/strong&gt; — no lag constraints, round-robin load balancing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl http://localhost:8000/replica
host-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Replica with a time constraint&lt;/strong&gt; — lag no greater than the threshold (default 1 second, configurable):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl http://localhost:8000/sync_by_time
host-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Replica with a WAL bytes constraint&lt;/strong&gt; — byte lag does not exceed the threshold (default 1 MB, configurable):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl http://localhost:8000/sync_by_bytes
host-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Replica synchronized by at least one dimension&lt;/strong&gt; — either by time or by bytes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl http://localhost:8000/sync_by_time_or_bytes
host-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Replica synchronized by both dimensions simultaneously&lt;/strong&gt; — both time and bytes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl http://localhost:8000/sync_by_time_and_bytes
host-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The most synchronized replica&lt;/strong&gt; — the one with the smallest byte lag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl http://localhost:8000/most_sync_by_bytes
host-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thresholds can be overridden directly in the request via query parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Replica with no more than 50 ms lag for this specific request&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="s1"&gt;'http://localhost:8000/sync_by_time?lag_ms=50'&lt;/span&gt;

&lt;span class="c"&gt;# Replica with no more than 10 KB lag&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="s1"&gt;'http://localhost:8000/sync_by_bytes?lag_bytes=10000'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets a single application use strict guarantees for critical queries and relaxed ones for analytics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important note:&lt;/strong&gt; if no suitable replica exists, any of these endpoints will return the master as a fallback. Application code always receives a host — it just may sometimes be the master instead of a replica.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solving Problem 3: Read-Your-Writes via &lt;code&gt;min_lsn&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This is the most interesting problem. Let's first understand what's happening.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;After a write to the master, the replica doesn't receive the data instantaneously — there is a delay between a transaction being committed on the master and being replayed on the replica. If you read from a replica immediately after a write, you may see stale data.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Solution: WAL LSN
&lt;/h3&gt;

&lt;p&gt;PostgreSQL knows the exact position in the transaction log (WAL LSN) up to which each replica has replayed data. pg-status polls this position and keeps it in memory.&lt;/p&gt;

&lt;p&gt;The pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;After writing to the master, obtain the LSN of that transaction via &lt;code&gt;pg_current_wal_lsn()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Pass this LSN to pg-status in the next read request&lt;/li&gt;
&lt;li&gt;pg-status returns only the replica that has already replayed data up to that position
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# After writing to the master, get the LSN:&lt;/span&gt;
&lt;span class="c"&gt;# SELECT pg_current_wal_lsn(); → '0/3000060'&lt;/span&gt;

&lt;span class="c"&gt;# Next read — only from a replica that has already replayed '0/3000060'&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="s1"&gt;'http://localhost:8000/replica?min_lsn=0/3000060'&lt;/span&gt;
host-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If no replica has caught up to the required position yet — the master is returned as a fallback. This is the correct behavior: it's better to read from the master than to show stale data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Python Integration
&lt;/h2&gt;

&lt;p&gt;Let me show what this looks like in a real Python application on FastAPI with async SQLAlchemy.&lt;/p&gt;

&lt;p&gt;For managing session lifecycle I'm using &lt;a href="https://github.com/krylosov-aa/context-async-sqlalchemy" rel="noopener noreferrer"&gt;context-async-sqlalchemy&lt;/a&gt; — a small library I wrote. It solves the following problem: storing a SQLAlchemy session in the current request context, automatically committing on successful response and rolling back on error. The key class is &lt;code&gt;DBConnect&lt;/code&gt;: an object that knows which host to connect to and lazily creates the engine on first access. It can be switched to a different host via &lt;code&gt;change_host()&lt;/code&gt; without restarting the application, or you can keep multiple &lt;code&gt;DBConnect&lt;/code&gt; instances, one per host.&lt;/p&gt;

&lt;h3&gt;
  
  
  pg-status Client
&lt;/h3&gt;

&lt;p&gt;A simple wrapper around HTTP — call the needed endpoint and return the hostname:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pg_status.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;aiohttp&lt;/span&gt;

&lt;span class="n"&gt;PG_STATUS_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_master_host&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;aiohttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PG_STATUS_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/master&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_replica_host&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;aiohttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PG_STATUS_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/most_sync_by_bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_all_hosts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Returns the names of all known hosts&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;aiohttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PG_STATUS_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/hosts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;host&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Connection Management
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;DBConnect&lt;/code&gt; from &lt;code&gt;context-async-sqlalchemy&lt;/code&gt; is an object that holds a SQLAlchemy engine for one host and lazily creates sessions for it. At application startup, we create one &lt;code&gt;DBConnect&lt;/code&gt; per host and store them in a dictionary. When a session is needed, we ask pg-status which host is current and retrieve the right &lt;code&gt;DBConnect&lt;/code&gt; from the dictionary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# database.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy.ext.asyncio&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;create_async_engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;async_sessionmaker&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;context_async_sqlalchemy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DBConnect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db_session&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;create_async_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgresql+asyncpg://user:password@&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:5432/mydb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;pool_pre_ping&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_session_maker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;async_sessionmaker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;class_&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expire_on_commit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="c1"&gt;# Dictionary of connections to all hosts — both master and replicas
&lt;/span&gt;&lt;span class="n"&gt;_connections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DBConnect&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;prepare_connections&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Called at application startup — create a DBConnect for each host&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;hosts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;get_all_hosts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# GET /hosts
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;hosts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;_connections&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DBConnect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;engine_creator&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;create_engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;session_maker_creator&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;create_session_maker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;master_session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;master_host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;get_master_host&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# GET /master
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;db_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_connections&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;master_host&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;replica_session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;replica_host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;get_replica_host&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# GET /most_sync_by_bytes
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;db_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_connections&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;replica_host&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what happens during a failover: pg-status detects the master change within one polling interval (5 seconds by default, configurable). The very next call to &lt;code&gt;master_session()&lt;/code&gt; gets the new host from pg-status and returns the already-existing &lt;code&gt;DBConnect&lt;/code&gt; for it from &lt;code&gt;_connections&lt;/code&gt;. No separate master object is needed — all connections live in one dictionary, and pg-status tells you which one to use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using in Handlers
&lt;/h3&gt;

&lt;p&gt;In the end, each handler simply picks the right session type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# handlers.py
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OrderData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;master_session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# write → always to the master
&lt;/span&gt;    &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_dump&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_order_list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;replica_session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# read → to a replica
&lt;/span&gt;    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scalars&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Read-Your-Writes
&lt;/h3&gt;

&lt;p&gt;For RYOW you need to pass the LSN to the client after a write, and at the next read use it to select a replica. It's convenient to organize this through two middlewares.&lt;/p&gt;

&lt;p&gt;The main advantage of this approach is that it works automatically for the entire application at once. Wire up the middleware once and every request that actually changed data automatically gets an LSN in the response cookie. No need to think about this when writing each new handler, no need to manually call &lt;code&gt;pg_current_wal_lsn()&lt;/code&gt; in business logic. The key word is "actually changed": &lt;code&gt;pg_current_xact_id_if_assigned()&lt;/code&gt; returns NULL for read-only transactions, so the cookie is only updated where there were actual writes — SELECT queries are not affected.&lt;/p&gt;

&lt;p&gt;One non-obvious detail is &lt;code&gt;_LsnHolder&lt;/code&gt;. Since &lt;code&gt;save_current_lsn_if_there_writes&lt;/code&gt; is called from the &lt;code&gt;before_commit&lt;/code&gt; hook and can't return a value to &lt;code&gt;lsn_cookie_middleware&lt;/code&gt; directly, shared mutable state is needed: &lt;code&gt;lsn_cookie_middleware&lt;/code&gt; puts an &lt;code&gt;_LsnHolder&lt;/code&gt; into a &lt;code&gt;ContextVar&lt;/code&gt;, and &lt;code&gt;save_current_lsn_if_there_writes&lt;/code&gt; retrieves the same object and writes the LSN into it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# read_own_writes.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;contextvars&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ContextVar&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy.ext.asyncio&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;starlette.middleware.base&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RequestResponseEndpoint&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;starlette.requests&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;starlette.responses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_LsnHolder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;


&lt;span class="n"&gt;_request_lsn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ContextVar&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;_LsnHolder&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ContextVar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_request_lsn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_request_lsn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_LsnHolder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_request_lsn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save_current_lsn_if_there_writes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Called before commit — saves the LSN if there was a write&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT pg_current_wal_lsn()::text &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WHERE pg_current_xact_id_if_assigned() IS NOT NULL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;lsn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scalar&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lsn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;get_request_lsn&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lsn&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lsn_cookie_middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RequestResponseEndpoint&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Initialize the holder at the start of each request
&lt;/span&gt;    &lt;span class="n"&gt;_request_lsn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_LsnHolder&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;call_next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# If this request had a write — send the LSN to the client via cookie
&lt;/span&gt;    &lt;span class="n"&gt;lsn_holder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_request_lsn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lsn_holder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Set-Cookie&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-WAL-LSN=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;lsn_holder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;; Path=/; SameSite=Lax; Secure&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire up both middlewares in the application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# setup_app.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;context_async_sqlalchemy.fastapi_utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;add_fastapi_http_db_session_middleware&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;starlette.middleware.base&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseHTTPMiddleware&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;read_own_writes&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;lsn_cookie_middleware&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;save_current_lsn_if_there_writes&lt;/span&gt;

&lt;span class="nf"&gt;add_fastapi_http_db_session_middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;before_commit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;save_current_lsn_if_there_writes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# captures LSN before commit
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseHTTPMiddleware&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dispatch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;lsn_cookie_middleware&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# sends LSN in cookie
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the read side — extract the LSN from the cookie and pass it to pg-status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;replica_session_ryow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;min_lsn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-WAL-LSN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;replica_host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;get_replica_host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min_lsn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;min_lsn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;db_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_connections&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;replica_host&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_replica_host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min_lsn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;min_lsn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;min_lsn&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;min_lsn&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;aiohttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PG_STATUS_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/most_sync_by_bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full flow: write request → LSN captured before commit → &lt;code&gt;X-WAL-LSN&lt;/code&gt; cookie set in response → client sends it in the next request → server reads the cookie and passes &lt;code&gt;min_lsn&lt;/code&gt; to pg-status → only the replica that has already replayed the required WAL position is returned, or the master if no replica has caught up yet.&lt;/p&gt;

&lt;h4&gt;
  
  
  Where to Store the LSN
&lt;/h4&gt;

&lt;p&gt;A cookie is a convenient option for browser clients: the browser sends them automatically with every request, no changes to client code are needed, the LSN is stored on the client side and doesn't require Redis or any shared storage. Scope is limited by path and expiry. But this is not the only option:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Response header + request header&lt;/strong&gt; — the server returns the LSN in &lt;code&gt;X-WAL-LSN&lt;/code&gt;, the client explicitly passes it back in the next request. Suitable for API clients and mobile applications.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis / shared storage&lt;/strong&gt; — the LSN is stored server-side under a &lt;code&gt;user_id&lt;/code&gt; or &lt;code&gt;session_id&lt;/code&gt; key. Needed if writes and reads happen on different application nodes — a cookie won't help in that case, the LSN must be placed in shared storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JWT / session token&lt;/strong&gt; — the LSN is included as a claim in the token. Convenient if the application already uses JWT sessions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Protecting the LSN in Production
&lt;/h4&gt;

&lt;p&gt;An LSN is simply a WAL position like &lt;code&gt;0/3000060&lt;/code&gt;. It's not secret in itself, but accepting it from the client without verification is dangerous: an attacker could pass an arbitrarily large LSN and force the server to always read from the master — no replica will ever reach such a position. In production you should sign the value with HMAC before sending it to the client and verify the signature on receipt — and only then pass the LSN to pg-status.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running and Configuration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Docker
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;pg_status__hosts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host-1,host-2,host-3 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;pg_status__pg_user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgres &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;pg_status__pg_password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgres &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 8000:8000 &lt;span class="se"&gt;\&lt;/span&gt;
  krylosovaa/pg-status:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After startup you can immediately verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl http://localhost:8000/master
host-1

&lt;span class="nv"&gt;$ &lt;/span&gt;curl http://localhost:8000/hosts | jq &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;
  &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"host"&lt;/span&gt;: &lt;span class="s2"&gt;"host-1"&lt;/span&gt;, &lt;span class="s2"&gt;"master"&lt;/span&gt;: &lt;span class="nb"&gt;true&lt;/span&gt;, &lt;span class="s2"&gt;"alive"&lt;/span&gt;: &lt;span class="nb"&gt;true&lt;/span&gt;, &lt;span class="s2"&gt;"lag_ms"&lt;/span&gt;: 0, ...&lt;span class="o"&gt;}&lt;/span&gt;,
  &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"host"&lt;/span&gt;: &lt;span class="s2"&gt;"host-2"&lt;/span&gt;, &lt;span class="s2"&gt;"master"&lt;/span&gt;: &lt;span class="nb"&gt;false&lt;/span&gt;, &lt;span class="s2"&gt;"alive"&lt;/span&gt;: &lt;span class="nb"&gt;true&lt;/span&gt;, &lt;span class="s2"&gt;"lag_ms"&lt;/span&gt;: 45, ...&lt;span class="o"&gt;}&lt;/span&gt;,
  &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"host"&lt;/span&gt;: &lt;span class="s2"&gt;"host-3"&lt;/span&gt;, &lt;span class="s2"&gt;"master"&lt;/span&gt;: &lt;span class="nb"&gt;false&lt;/span&gt;, &lt;span class="s2"&gt;"alive"&lt;/span&gt;: &lt;span class="nb"&gt;true&lt;/span&gt;, &lt;span class="s2"&gt;"lag_ms"&lt;/span&gt;: 120, ...&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key Parameters
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pg_status__hosts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Comma-separated list of hosts (required)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pg_status__sleep_ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5000&lt;/td&gt;
&lt;td&gt;Host polling interval in ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pg_status__sync_max_lag_ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;Lag threshold for &lt;code&gt;sync_by_time&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pg_status__sync_max_lag_bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1000000&lt;/td&gt;
&lt;td&gt;Lag threshold for &lt;code&gt;sync_by_bytes&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pg_status__max_fails&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Consecutive failures before a host is declared dead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pg_status__query_timeout_ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5000&lt;/td&gt;
&lt;td&gt;Timeout for a single host poll in ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Where to Get It
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://hub.docker.com/r/krylosovaa/pg-status" rel="noopener noreferrer"&gt;Docker Hub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/krylosov-aa/pg-status/releases/latest" rel="noopener noreferrer"&gt;deb package&lt;/a&gt; for Debian/Ubuntu&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/krylosov-aa/pg-status/releases/latest" rel="noopener noreferrer"&gt;Static binary&lt;/a&gt; for direct execution&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/krylosov-aa/pg-status" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; — can be compiled via CMake for any platform&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Working with PostgreSQL master/replica on the application side comes down to three problems: find the master, find a suitable replica, ensure read-your-writes. pg-status solves all three through a simple HTTP API that responds from memory in fractions of a millisecond.&lt;/p&gt;

&lt;p&gt;Happy to answer questions in the comments. If the article was useful — a star on &lt;a href="https://github.com/krylosov-aa/pg-status" rel="noopener noreferrer"&gt;pg-status&lt;/a&gt; or &lt;a href="https://github.com/krylosov-aa/context-async-sqlalchemy" rel="noopener noreferrer"&gt;context-async-sqlalchemy&lt;/a&gt; is a great motivator to keep developing both projects.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>sql</category>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>pg-status - a lightweight microservice for checking PostgreSQL host status</title>
      <dc:creator>krylosov-aa</dc:creator>
      <pubDate>Sun, 04 Jan 2026 22:54:32 +0000</pubDate>
      <link>https://dev.to/krylosov-aa/pg-status-a-lightweight-microservice-for-checking-postgresql-host-status-32jd</link>
      <guid>https://dev.to/krylosov-aa/pg-status-a-lightweight-microservice-for-checking-postgresql-host-status-32jd</guid>
      <description>&lt;p&gt;&lt;strong&gt;Hi!&lt;/strong&gt; I’d like to introduce my new project — &lt;a href="https://github.com/krylosov-aa/pg-status" rel="noopener noreferrer"&gt;&lt;strong&gt;pg-status&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It’s a lightweight, high-performance microservice designed to determine the status of PostgreSQL hosts. Its main goal is to help your backend identify a live master and a sufficiently up-to-date synchronous replica.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Very easy to deploy as a sidecar and integrate with your existing PostgreSQL setup&lt;/li&gt;
&lt;li&gt;Identifies the master and synchronous replicas, and assists with failover&lt;/li&gt;
&lt;li&gt;Helps balance load between hosts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you find this project useful, I’d really appreciate your support — a &lt;a href="https://github.com/krylosov-aa/pg-status" rel="noopener noreferrer"&gt;star on GitHub &lt;/a&gt; would mean a lot!&lt;/p&gt;

&lt;p&gt;But first, let’s talk about the problem &lt;strong&gt;pg-status&lt;/strong&gt; is built to solve.&lt;/p&gt;

&lt;h1&gt;
  
  
  PostgreSQL on multiple hosts
&lt;/h1&gt;

&lt;p&gt;To improve the resilience and scalability of a PostgreSQL database, it’s common to run multiple hosts using the classic master–replica setup. There’s one &lt;strong&gt;master&lt;/strong&gt; host that accepts writes, and one or more &lt;strong&gt;replicas&lt;/strong&gt; that receive changes from the master via physical or logical replication.&lt;/p&gt;

&lt;p&gt;Everything works great in theory — but there are a few important details to consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Any host can fail&lt;/li&gt;
&lt;li&gt;A replica may need to take over as the master (failover)&lt;/li&gt;
&lt;li&gt;A replica can significantly lag behind the master&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From the perspective of a backend application connecting to these databases, this introduces several practical challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to determine which host is currently the live master&lt;/li&gt;
&lt;li&gt;How to identify which replicas are available&lt;/li&gt;
&lt;li&gt;How to measure replica lag to decide whether it’s suitable for reads&lt;/li&gt;
&lt;li&gt;How to switch the client connection pool (or otherwise handle reconnection) after failover&lt;/li&gt;
&lt;li&gt;How to distribute load effectively among hosts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are already various approaches to solving these problems — each with its own pros and cons. Here are a few of the common methods I’ve encountered:&lt;/p&gt;

&lt;h3&gt;
  
  
  Via DNS
&lt;/h3&gt;

&lt;p&gt;In this approach, specific hostnames point to the master and replica instances. Essentially, there’s no built-in master failover handling, and it doesn’t help determine the replica status — you have to query it manually via SQL.&lt;/p&gt;

&lt;p&gt;It’s possible to add an external service that detects host states and updates the DNS records accordingly, but there are a few drawbacks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DNS updates can take several seconds — or even tens of seconds — which can be critical&lt;/li&gt;
&lt;li&gt;DNS might automatically switch to read-only mode&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Overall, this solution &lt;em&gt;does&lt;/em&gt; work, and &lt;code&gt;pg-status&lt;/code&gt; can actually serve as such a service for host state detection.&lt;/p&gt;

&lt;p&gt;Also, as far as I know, many PostgreSQL cloud providers rely on this exact mechanism.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multihost in libpq
&lt;/h3&gt;

&lt;p&gt;With this method, the client driver (libpq) can locate the first (or random) available host from a given list that matches the desired role (master or replica).&lt;/p&gt;

&lt;p&gt;A change in the master is detected only after an actual SQL query fails — at which point the connection crashes, and the client cycles through the hosts list again upon reconnection.&lt;/p&gt;

&lt;h3&gt;
  
  
  Proxy
&lt;/h3&gt;

&lt;p&gt;You can set up a proxy that supports on-the-fly configuration updates. In that case, you’ll also need some component responsible for notifying the proxy when it should switch to a different host.&lt;/p&gt;

&lt;p&gt;This is generally a solid approach, but it still depends on an external mechanism that monitors PostgreSQL host states and communicates those changes to the proxy. &lt;code&gt;pg-status&lt;/code&gt; fits perfectly for this purpose — it can serve as that mechanism.&lt;/p&gt;

&lt;p&gt;Alternatively, you can use &lt;strong&gt;pgpool-II&lt;/strong&gt;, which is specifically designed for such scenarios. It not only determines which host to route traffic to but can even perform automatic failover itself. The main downside, however, is that it can be complex to deploy and configure.&lt;/p&gt;

&lt;h3&gt;
  
  
  CloudNativePG
&lt;/h3&gt;

&lt;p&gt;As far as I know, CloudNativePG already provides all this functionality out of the box. The main considerations here are deployment complexity and the requirement to run within a Kubernetes environment.&lt;/p&gt;

&lt;h1&gt;
  
  
  My solution - pg-status
&lt;/h1&gt;

&lt;p&gt;At my workplace, we use a PostgreSQL cloud provider that offers a built-in failover mechanism and lets us connect to the master via DNS. However, I wanted to avoid situations where DNS updates take too long to reflect the new master.&lt;/p&gt;

&lt;p&gt;I also wanted more control — not just connecting to the master, but also balancing read load across replicas and understanding how far each replica lags behind the master. At the same time, I didn’t want to complicate the system architecture with a shared proxy that could become a single point of failure.&lt;/p&gt;

&lt;p&gt;In the end, the ideal solution turned out to be a tiny sidecar service running next to the backend. This sidecar takes responsibility for selecting the appropriate host. On the backend side, I maintain a client connection pool and, before issuing a connection, I check the current host status and immediately reconnect to the right one if needed.&lt;/p&gt;

&lt;p&gt;The sidecar approach brings some extra benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A sidecar failure affects only the single instance it’s attached to, not the entire system.&lt;/li&gt;
&lt;li&gt;PostgreSQL availability is measured relative to the local instance — meaning the health check can automatically report that this instance shouldn't receive traffic if the database is unreachable (for example, due to network isolation between data centers).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s how &lt;strong&gt;pg-status&lt;/strong&gt; was born. Its job is to periodically poll PostgreSQL hosts, keep track of their current state, and expose several lightweight, fast endpoints for querying this information.&lt;/p&gt;

&lt;p&gt;You can call &lt;strong&gt;pg-status&lt;/strong&gt; directly from your backend on each request — for example, to make sure the master hasn’t failed over, and if it has, to reconnect automatically. Alternatively, you can use its special endpoints to select an appropriate replica for read operations based on replication lag.&lt;/p&gt;

&lt;p&gt;For example, I have a library for Python - &lt;a href="https://github.com/krylosov-aa/context-async-sqlalchemy" rel="noopener noreferrer"&gt;context-async-sqlalchemy&lt;/a&gt;, which &lt;a href="https://krylosov-aa.github.io/context-async-sqlalchemy/master_replica/" rel="noopener noreferrer"&gt;has a special place&lt;/a&gt;, where you can user pg-status to always get to the right host.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to use
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;

&lt;p&gt;You can build &lt;strong&gt;pg-status&lt;/strong&gt; from source, install it from a &lt;code&gt;.deb&lt;/code&gt; or binary package, or run it as a Docker container (lightweight Alpine-based images are available&amp;nbsp;or ubuntu-based). Currently, the target architecture is &lt;strong&gt;Linux amd64&lt;/strong&gt;, but the microservice can be compiled for other targets using &lt;strong&gt;CMake&lt;/strong&gt; if needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Usage
&lt;/h3&gt;

&lt;p&gt;The service’s behavior is configured via &lt;strong&gt;environment variables&lt;/strong&gt;. Some variables are required (for example, connection parameters for your PostgreSQL hosts), while others are optional and have default values.&lt;/p&gt;

&lt;p&gt;You can find the full list of parameters here: &lt;a href="https://github.com/krylosov-aa/pg-status?tab=readme-ov-file#parameters" rel="noopener noreferrer"&gt;https://github.com/krylosov-aa/pg-status?tab=readme-ov-file#parameters&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When running, &lt;strong&gt;pg-status&lt;/strong&gt; exposes several simple HTTP endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /master&lt;/code&gt; - returns the current master&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /replica&lt;/code&gt; - returns a random replica using the round-robin algorithm&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /sync_by_time&lt;/code&gt; - returns a synchronous replica based on time or the master, meaning the lag behind the master is measured in time&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /sync_by_bytes&lt;/code&gt; - returns a synchronous replica based on bytes (based on the WAL LSN log) or the master, meaning the lag behind the master is measured in bytes written to the log&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /sync_by_time_or_bytes&lt;/code&gt; - essentially a host from sync_by_time or from sync_by_bytes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /sync_by_time_and_bytes&lt;/code&gt; - essentially a host from sync_by_time and From sync_by_bytes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /hosts&lt;/code&gt; - returns a list of all hosts and their current status: live, master, or replica.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As you can see, &lt;strong&gt;pg-status&lt;/strong&gt; provides a flexible API for identifying the appropriate replica to use. You can also set maximum acceptable lag thresholds (in time or bytes) via environment variables.&lt;/p&gt;

&lt;p&gt;Almost all endpoints support two response modes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Plain text (default)&lt;/li&gt;
&lt;li&gt;JSON — when you include the header&amp;nbsp;&lt;code&gt;Accept: application/json&lt;/code&gt; For example: &lt;code&gt;{"host": "localhost"}&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;pg-status&lt;/strong&gt; can also work alongside a &lt;strong&gt;proxy&lt;/strong&gt; or any other solution responsible for handling database connections. In this setup, your backend always connects to a single proxy host (for instance, one that points to the master). The proxy itself doesn’t know the current PostgreSQL state — instead, it queries &lt;strong&gt;pg-status&lt;/strong&gt; via its HTTP endpoints to decide when to switch to a different host.&lt;/p&gt;

&lt;h2&gt;
  
  
  pg-status Implementation Details
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;pg-status&lt;/strong&gt; is a microservice written in &lt;strong&gt;C&lt;/strong&gt;. I chose this language for two main reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It’s extremely resource-efficient — perfect for a lightweight sidecar scenario&lt;/li&gt;
&lt;li&gt;I simply enjoy writing in C, and this project felt like a natural fit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The microservice consists of two core components running in two active threads:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. PG Monitoring
&lt;/h3&gt;

&lt;p&gt;The first thread is responsible for monitoring. It periodically polls all configured hosts using the &lt;strong&gt;libpq&lt;/strong&gt; library to determine their current status. This part has an extensive list of configurable parameters, all set via environment variables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How often to poll hosts&lt;/li&gt;
&lt;li&gt;Connection timeout for each host&lt;/li&gt;
&lt;li&gt;Number of failed connection attempts before marking a host as dead&lt;/li&gt;
&lt;li&gt;Maximum acceptable replica lag (in milliseconds) considered “synchronous”&lt;/li&gt;
&lt;li&gt;Maximum acceptable replica lag (in bytes, based on WAL LSN) considered “synchronous”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Currently, only &lt;strong&gt;physical replication&lt;/strong&gt; is supported.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. HTTP Server
&lt;/h3&gt;

&lt;p&gt;The second thread runs the &lt;strong&gt;HTTP server&lt;/strong&gt;, which handles client requests and retrieves the current host status from memory. It’s implemented using &lt;a href="https://www.gnu.org/software/libmicrohttpd/" rel="noopener noreferrer"&gt;&lt;strong&gt;libmicrohttpd&lt;/strong&gt;&lt;/a&gt;, offering great performance while keeping the footprint small.&lt;/p&gt;

&lt;p&gt;This means your backend can safely query &lt;strong&gt;pg-status&lt;/strong&gt; before every SQL operation without noticeable overhead.&lt;/p&gt;

&lt;p&gt;In my testing (in a Docker container limited to 0.1 CPU and 6 MB of RAM), I achieved around &lt;strong&gt;1500 RPS&lt;/strong&gt; with extremely low latency. You can see detailed performance metrics &lt;a href="https://github.com/krylosov-aa/pg-status?tab=readme-ov-file#performance" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Potential Improvements
&lt;/h3&gt;

&lt;p&gt;Right now, I’m happy with the functionality — &lt;strong&gt;pg-status&lt;/strong&gt; is already used in production in my own projects. That said, some improvements I’m considering include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Support for&amp;nbsp;&lt;strong&gt;logical replication&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Adding precise time and byte lag information directly to the JSON responses so clients can make more informed decisions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you find the project interesting or have ideas for enhancements, feel free to open an issue on GitHub — contributions and feedback are always welcome!&lt;/p&gt;

&lt;h1&gt;
  
  
  Summary
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;pg-status&lt;/strong&gt; is a lightweight, efficient microservice designed to solve a practical problem — determining the status of PostgreSQL hosts — while being exceptionally easy to deploy and operate.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Licensed under&amp;nbsp;&lt;strong&gt;MIT&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Open source and available on GitHub:&amp;nbsp;&lt;a href="https://github.com/krylosov-aa/pg-status" rel="noopener noreferrer"&gt;https://github.com/krylosov-aa/pg-status&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Available as source,&amp;nbsp;&lt;code&gt;.deb&lt;/code&gt; binary package, or Docker container&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you like the project, I’d really appreciate your support — please ⭐ it on GitHub!&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>sql</category>
      <category>c</category>
    </item>
    <item>
      <title>context-async-sqlalchemy - The best way to use sqlalchemy in an async python application</title>
      <dc:creator>krylosov-aa</dc:creator>
      <pubDate>Tue, 09 Dec 2025 09:49:17 +0000</pubDate>
      <link>https://dev.to/krylosov-aa/context-async-sqlalchemy-the-best-way-to-use-sqlalchemy-in-an-async-python-application-c1b</link>
      <guid>https://dev.to/krylosov-aa/context-async-sqlalchemy-the-best-way-to-use-sqlalchemy-in-an-async-python-application-c1b</guid>
      <description>&lt;p&gt;Hello! I’d like to introduce my new library - &lt;a href="https://github.com/krylosov-aa/context-async-sqlalchemy" rel="noopener noreferrer"&gt;&lt;strong&gt;context-async-sqlalchemy&lt;/strong&gt;&lt;/a&gt;. It makes working with &lt;strong&gt;SQLAlchemy&lt;/strong&gt; in asynchronous Python applications incredibly easy. The library requires minimal code for simple use cases, yet offers maximum flexibility for more complex scenarios.&lt;/p&gt;

&lt;p&gt;Let’s briefly review the theory behind SQLAlchemy - what it consists of and how it integrates into a Python application. We’ll explore some of the nuances and see how &lt;a href="https://github.com/krylosov-aa/context-async-sqlalchemy" rel="noopener noreferrer"&gt;&lt;strong&gt;context-async-sqlalchemy&lt;/strong&gt;&lt;/a&gt; helps you work with it more conveniently. Note that everything here refers to &lt;strong&gt;asynchronous Python&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Short Summary of SQLAlchemy
&lt;/h2&gt;

&lt;p&gt;SQLAlchemy provides an &lt;strong&gt;Engine&lt;/strong&gt;, which manages the database connection pool, and a &lt;strong&gt;Session&lt;/strong&gt;, through which SQL queries are executed. Each session uses a single connection that it obtains from the engine.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frmb6wd62p0bok1o2fgva.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frmb6wd62p0bok1o2fgva.png" alt="Rough diagram" width="800" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The engine should have a long lifespan to keep the connection pool active. Sessions, on the other hand, should be short-lived, returning their connections to the pool as quickly as possible.&lt;/p&gt;

&lt;p&gt;(In the diagram, both engines are shown connected to the same database - which is admittedly unusual. The goal is simply to illustrate that each engine has its own connection pool. In practice, your application will typically use one engine per database - for example, one for the master and another for the replica.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Integration and Usage in an Application
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Direct Usage
&lt;/h3&gt;

&lt;p&gt;Let’s start with the simplest manual approach - using only SQLAlchemy, which can be integrated anywhere.&lt;/p&gt;

&lt;p&gt;Create an engine and a session maker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_async_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;session_maker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;async_sessionmaker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expire_on_commit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now imagine we have an endpoint for creating a user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/users/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;session_maker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On line 2, we open a session; on line 3, we begin a transaction; and finally, on line 4, we execute some SQL to create a user.&lt;/p&gt;

&lt;p&gt;Now imagine that, as part of the user creation process, we need to execute two SQL queries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/users/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;insert_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;insert_user_profile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;insert_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;session_maker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;insert_user_profile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;session_maker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we encounter two problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Two transactions are being used, even though we probably want only one.&lt;/li&gt;
&lt;li&gt;Code duplication.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We can try to fix this by moving the context managers to a higher level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/users/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;session_maker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;insert_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;insert_user_profile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;insert_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;insert_user_profile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But if we look at multiple handlers, the duplication still remains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/dogs/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_dog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;session_maker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/cats&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_cat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;session_maker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Dependency Injection
&lt;/h3&gt;

&lt;p&gt;You can move session and transaction management into a dependency. For example, in &lt;strong&gt;FastAPI&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_atomic_session&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;session_maker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;


&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/dogs/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_dog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_atomic_session&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/cats/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_cat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_atomic_session&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code duplication is gone, but now the session and transaction remain open until the end of the request lifecycle, with no way to close them early and release the connection back to the pool.&lt;/p&gt;

&lt;p&gt;This could be solved by returning a DI container from the dependency that manages sessions - however, that approach adds complexity, and no ready‑made solutions exist.&lt;/p&gt;

&lt;p&gt;Additionally, the session now has to be passed through multiple layers of function calls, even to those that don’t directly need it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/some_handler/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;some_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_atomic_session&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;do_first&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;do_second&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;do_first&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;do_something&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;insert_to_database&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;insert_to_database&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, &lt;code&gt;do_first&lt;/code&gt; doesn’t directly use the session but still has to accept and pass it along. Personally, I find this inelegant - I prefer to encapsulate that logic inside &lt;code&gt;insert_to_database&lt;/code&gt;. It’s a matter of taste and philosophy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrappers Around SQLAlchemy
&lt;/h3&gt;

&lt;p&gt;There are various wrappers around SQLAlchemy that offer convenience but introduce new syntax - something I find undesirable. Developers already familiar with SQLAlchemy shouldn’t have to learn an entirely new API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The New Library
&lt;/h2&gt;

&lt;p&gt;I wasn’t satisfied with the existing approaches. In my FastAPI service, I didn’t want to write excessive boilerplate just to work comfortably with SQL. I needed a minimal‑code solution that still allowed flexible session and transaction control - but couldn’t find one. So I built it for myself, and now I’m sharing it with the world.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My goals for the library were:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Minimal boilerplate and no code duplication&lt;/li&gt;
&lt;li&gt;Automatic commit or rollback when manual control isn’t required&lt;/li&gt;
&lt;li&gt;The ability to manually manage sessions and transactions when needed&lt;/li&gt;
&lt;li&gt;Suitable for both simple CRUD operations and complex logic&lt;/li&gt;
&lt;li&gt;No new syntax - pure SQLAlchemy&lt;/li&gt;
&lt;li&gt;Framework‑agnostic design&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s the result.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simplest Scenario
&lt;/h3&gt;

&lt;p&gt;To make a single SQL query inside a handler - without worrying about sessions or transactions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;context_async_sqlalchemy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;db_session&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;some_func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;db_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# new session
&lt;/span&gt;    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# some sql query
&lt;/span&gt;
    &lt;span class="c1"&gt;# commit automatically
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;db_session&lt;/code&gt; function automatically creates (or reuses) a session and closes it when the request ends.&lt;/p&gt;

&lt;p&gt;Multiple queries within one transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/users/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;insert_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;insert_user_profile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;insert_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;db_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# creates a session
&lt;/span&gt;    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# opens a connection and a transaction
&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;insert_user_profile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;db_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# gets the same session
&lt;/span&gt;    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# uses the same connection and transaction
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Early Commit
&lt;/h3&gt;

&lt;p&gt;Need to commit early? You can:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;manual_commit_example&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;db_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# manually commit the transaction
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, for example, consider the following scenario: you have a function called &lt;code&gt;insert_something&lt;/code&gt; that’s used in one handler where an autocommit at the end of the query is fine. Now you want to reuse &lt;code&gt;insert_something&lt;/code&gt; in another handler that requires an early commit. You don’t need to modify &lt;code&gt;insert_something&lt;/code&gt; at all - you can simply do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;example_1&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;insert_something&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# autocommit is suitable for us here
&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;example_2&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;insert_something&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# here we want to make a commit before the update
&lt;/span&gt;    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;commit_db_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# commits the context transaction
&lt;/span&gt;    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;update_something&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# works with a new transaction
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, even better, you can do it this way - by wrapping the function in a separate transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;example_2&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;atomic_db_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# a transaction is opened and closed
&lt;/span&gt;        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;insert_something&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;update_something&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# works with a new transaction
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also perform an early rollback using &lt;code&gt;rollback_db_session&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Early Session Close
&lt;/h3&gt;

&lt;p&gt;There are situations where you may need to close a session to release its connection - for example, while performing other long‑running operations.&lt;br&gt;
You can do it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;example_with_long_work&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;atomic_db_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;insert_something&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;close_db_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# released the connection
&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="c1"&gt;# some very long work here
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;update_something&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;close_db_session&lt;/code&gt; closes the current session. When &lt;code&gt;update_something&lt;/code&gt; calls &lt;code&gt;db_session&lt;/code&gt;, it will already have a new session with a different connection.&lt;/p&gt;

&lt;h3&gt;
  
  
  Concurrent Queries
&lt;/h3&gt;

&lt;p&gt;In SQLAlchemy, you can’t run two concurrent queries within the same session. To do so, you need to create a separate session.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;concurent_example&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;insert_something&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;some_args&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;insert_another_thing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;some_args&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# error!
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The library provides two simple ways to execute concurrent queries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;concurent_example&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;insert_something&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;some_args&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;run_in_new_ctx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;  &lt;span class="c1"&gt;# separate session with autocommit
&lt;/span&gt;            &lt;span class="n"&gt;insert_another_thing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;some_args&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;run_in_new_ctx&lt;/code&gt; runs a function in a new context, giving it a fresh session. This can be used, for example, with functions executed via &lt;code&gt;asyncio.gather&lt;/code&gt; or &lt;code&gt;asyncio.create_task&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Alternatively, you can work with a session entirely outside of any context - just like in the manual mode described at the beginning.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;insert_another_thing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;some_args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;new_non_ctx_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# or
&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;insert_something&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;some_args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;new_non_ctx_atomic_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These methods can be combined:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;_insert&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;  &lt;span class="c1"&gt;# context session
&lt;/span&gt;    &lt;span class="nf"&gt;run_in_new_ctx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_insert&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# new context session
&lt;/span&gt;    &lt;span class="nf"&gt;_insert_non_ctx&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;  &lt;span class="c1"&gt;# own manual session
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Other Scenarios
&lt;/h3&gt;

&lt;p&gt;The repository includes several application integration examples. You can also explore &lt;a href="https://github.com/krylosov-aa/context-async-sqlalchemy/tree/main/examples/fastapi_example/routes" rel="noopener noreferrer"&gt;various scenarios for using the library&lt;/a&gt;. These scenarios also serve as tests for the library - verifying its behavior within a real application context rather than in isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrating the Library with Your Application
&lt;/h2&gt;

&lt;p&gt;Now let’s look at how to integrate this library into your application. The goal was to make the process as simple as possible.&lt;/p&gt;

&lt;p&gt;We’ll start by creating the &lt;code&gt;engine&lt;/code&gt; and &lt;code&gt;session_maker&lt;/code&gt;, and by addressing the &lt;code&gt;connect&lt;/code&gt; parameter, which is passed throughout the library functions. The &lt;code&gt;DBConnect&lt;/code&gt; class is responsible for managing the database connection configuration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;context_async_sqlalchemy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DBConnect&lt;/span&gt;

&lt;span class="n"&gt;connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DBConnect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;engine_creator&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;create_engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;session_maker_creator&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;create_session_maker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The intended use is to have a global instance responsible for managing the lifecycle of the &lt;code&gt;engine&lt;/code&gt; and &lt;code&gt;session_maker&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It takes two factory functions as input:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;engine_creator&lt;/code&gt;&lt;/strong&gt;&amp;nbsp;- a factory function for creating the&amp;nbsp;&lt;code&gt;engine&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;session_maker_creator&lt;/code&gt;&lt;/strong&gt;&amp;nbsp;- a factory function for creating the&amp;nbsp;&lt;code&gt;session_maker&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here are some examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;pg_user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;krylosov-aa&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;pg_password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="n"&gt;pg_port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6432&lt;/span&gt;
    &lt;span class="n"&gt;pg_db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;create_async_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgresql+asyncpg://&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pg_user&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pg_password&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pg_port&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pg_db&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;pool_pre_ping&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_session_maker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;async_sessionmaker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;class_&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expire_on_commit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;host&lt;/code&gt; is an optional parameter that specifies the database host to connect to.&lt;/p&gt;

&lt;p&gt;Why is the host optional, and why use factories? Because the library allows you to reconnect to the database at runtime - which is especially useful when working with a master and replica setup.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DBConnect&lt;/code&gt; also has another optional parameter - a handler that is called before creating a new session. You can place any custom logic there, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;renew_master_connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DBConnect&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;master_host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;get_master&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;# determine the master host
&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;master_host&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# if the host has changed
&lt;/span&gt;        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;change_host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master_host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# reconnecting
&lt;/span&gt;

&lt;span class="n"&gt;master&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DBConnect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;

    &lt;span class="c1"&gt;# handler before session creation
&lt;/span&gt;    &lt;span class="n"&gt;before_create_session_handler&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;renew_master_connect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;replica&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DBConnect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="n"&gt;before_create_session_handler&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;renew_replica_connect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the end of your application's lifecycle, you should gracefully close the connection. &lt;code&gt;DBConnect&lt;/code&gt; provides a &lt;code&gt;close()&lt;/code&gt; method for this purpose.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@asynccontextmanager&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lifespan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# some application startup logic
&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt;

    &lt;span class="c1"&gt;# application termination logic
&lt;/span&gt;    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# closing the connection to the database
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All the important logic and “magic” of session and transaction management is handled by the middleware - and it’s very easy to set up.&lt;/p&gt;

&lt;p&gt;Here’s an example for &lt;strong&gt;FastAPI&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;context_async_sqlalchemy.fastapi_utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;add_fastapi_http_db_session_middleware&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="nf"&gt;add_fastapi_http_db_session_middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is also pure ASGI middleware.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;context_async_sqlalchemy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ASGIHTTPDBSessionMiddleware&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ASGIHTTPDBSessionMiddleware&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;p&gt;Testing is a crucial part of development. I prefer to test using a real, live PostgreSQL database. In this case, there’s one key issue that needs to be addressed - &lt;strong&gt;data isolation between tests&lt;/strong&gt;. There are essentially two approaches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Clearing data between tests.&lt;/strong&gt; In this setup, the application uses its own transaction, and the test uses a separate one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Using a shared transaction&lt;/strong&gt; between the test and the application and performing rollbacks to restore the state.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first approach is very convenient for debugging, and sometimes it’s the only practical option - for example, when testing complex scenarios involving multiple transactions or concurrent queries. It’s also a &lt;em&gt;“fair”&lt;/em&gt; testing method because it checks how the application actually handles sessions.&lt;/p&gt;

&lt;p&gt;However, it has a downside: such tests take longer to run because of the time required to clear data between them - even when using &lt;code&gt;TRUNCATE&lt;/code&gt; statements, which still have to process all tables.&lt;/p&gt;

&lt;p&gt;The second approach, on the other hand, is much faster thanks to rollbacks, but it’s not as realistic since we must prepare the session and transaction for the application in advance.&lt;/p&gt;

&lt;p&gt;Don't forget to give the project a star on GitHub if you like it! Thanks!&lt;/p&gt;

&lt;p&gt;In my projects, I use both approaches together: a shared transaction for most tests with simple logic, and separate transactions for the minority of more complex scenarios.&lt;/p&gt;

&lt;p&gt;The library provides a few utilities that make testing easier. The first is &lt;strong&gt;&lt;code&gt;rollback_session&lt;/code&gt;&lt;/strong&gt; - a session that is always rolled back at the end. It’s useful for both types of tests and helps maintain a clean, isolated test environment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@pytest_asyncio.fixture&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;db_session_test&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;rollback_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For tests that use shared transactions, the library provides two utilities: &lt;code&gt;set_test_context&lt;/code&gt; and &lt;code&gt;put_savepoint_session_in_ctx&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@pytest_asyncio.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;autouse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;db_session_override&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db_session_test&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;set_test_context&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;put_savepoint_session_in_ctx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db_session_test&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This fixture creates a context in advance, so the application runs within it instead of creating its own. The context also contains a pre‑initialized session that creates a &lt;strong&gt;release savepoint&lt;/strong&gt; instead of performing a commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it all works
&lt;/h2&gt;

&lt;p&gt;Here's a diagram of how it all works:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhabrastorage.org%2Fgetpro%2Fhabr%2Fupload_files%2F4db%2Fd1a%2F944%2F4dbd1a9449ac67b7daa03f5ac31044c8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhabrastorage.org%2Fgetpro%2Fhabr%2Fupload_files%2F4db%2Fd1a%2F944%2F4dbd1a9449ac67b7daa03f5ac31044c8.png" title="Как все работает" alt="how it all works" width="761" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The middleware initializes the context, and your application accesses it through the library’s functions. Finally, the middleware closes any remaining open resources and then cleans up the context itself.&lt;/p&gt;

&lt;p&gt;How the middleware works:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhabrastorage.org%2Fgetpro%2Fhabr%2Fupload_files%2Ff21%2Fce3%2Fe99%2Ff21ce3e9997b1023e07a8c8b8ad6f88b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhabrastorage.org%2Fgetpro%2Fhabr%2Fupload_files%2Ff21%2Fce3%2Fe99%2Ff21ce3e9997b1023e07a8c8b8ad6f88b.png" title="как работает middleware" alt="How the middleware works" width="800" height="256"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The context we’ve been talking about is a &lt;code&gt;ContextVar&lt;/code&gt;. It stores a mutable container, and when your application accesses the library to obtain a session, the library operates on that container. Because the container is mutable, sessions and transactions can be closed early. The middleware then operates only on what remains open within the container.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Let’s summarize. We’ve built a great library that makes working with &lt;strong&gt;SQLAlchemy&lt;/strong&gt; in asynchronous applications simple and enjoyable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Minimal code, no duplication&lt;/li&gt;
&lt;li&gt;Automatic commit or rollback - no need for manual management&lt;/li&gt;
&lt;li&gt;Full support for manual session and transaction control when needed&lt;/li&gt;
&lt;li&gt;Convenient for both CRUD operations and advanced use cases&lt;/li&gt;
&lt;li&gt;No new syntax - pure SQLAlchemy&lt;/li&gt;
&lt;li&gt;Framework‑agnostic&lt;/li&gt;
&lt;li&gt;Easy to test&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use it!&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Licensed under the&amp;nbsp;&lt;strong&gt;MIT License&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Open‑source code on GitHub:&amp;nbsp;&lt;a href="https://github.com/krylosov-aa/context-async-sqlalchemy" rel="noopener noreferrer"&gt;github.com/krylosov-aa/context-async-sqlalchemy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Documentation:&amp;nbsp;&lt;a href="https://krylosov-aa.github.io/context-async-sqlalchemy/" rel="noopener noreferrer"&gt;krylosov-aa.github.io/context-async-sqlalchemy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Available on PyPI:&amp;nbsp;&lt;a href="https://pypi.org/project/context-async-sqlalchemy/" rel="noopener noreferrer"&gt;pypi.org/project/context-async-sqlalchemy&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’m using this library in a real production environment - so feel free to use it in your own projects as well! Your feedback is always welcome - I’m open to improvements, refinements, and suggestions.&lt;/p&gt;

&lt;p&gt;Don't forget to give the project a star on GitHub if you like it! Thanks!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>python</category>
      <category>sql</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
