Resolving 'cannot execute UPDATE in a read-only transaction' on transaction pooler connections
Last edited: 8/12/2026
关键技术术语 #
🌐 Key technical terms
事务连接池 这是 Supavisor 或 PgBouncer 之类工具使用的一种连接管理方法。它不像给每个客户端分配一个专用的、永久的数据库连接,而是维护一小部分“后端连接”。在一个事务期间,它会把其中一个连接借给客户端用,然后事务结束后立即收回,再借给另一个客户端。
后端连接 Postgres 服务器上实际执行你查询的物理进程。在连接池环境中,一个后端连接在其生命周期内会为许多不同的数据库客户端提供服务。
会话级状态 那个后端连接有状态。Postgres 连接会带上一些设置,比如时区、内存限制、搜索路径、只读模式等等。在每个客户端都有自己连接的正常设置下,这不是什么问题。客户端断开连接时,连接和所有状态都会消失。但在连接池环境中,连接不会消失。它会回到池里,所有设置都保持不变,下一位使用它的客户端就会继承之前留下的状态。
理解问题:‘粘滞’状态 #
🌐 Understanding the problem: The "sticky" state
当你在使用事务池(通常在端口 6543)时遇到错误 cannot execute UPDATE in a read-only transaction,即使你已经确认连接的是主数据库,并且数据库本身没有处于只读模式(可以通过端口 5432 的直接连接用 SHOW default_transaction_read_only; 或 SELECT pg_is_in_recovery(); 检查),这意味着一个后台连接意外地被锁定到了只读状态。
🌐 When you encounter the error cannot execute UPDATE in a read-only transaction while using a transaction pooler (typically on port 6543), even when you have verified that the connection is made to the primary database and the database itself is not in read-only mode (check with SHOW default_transaction_read_only; or SELECT pg_is_in_recovery(); using a direct connection on port 5432), it signifies that a backend connection has been unintentionally locked into a read-only state.
注意: 如果你的数据库处于只读模式(例如,由于超过磁盘空间限制),你可以查看本指南:数据库大小与只读模式。本指南剩余部分讨论的是事务池专属的不同问题。
原因:连接被污染 #
🌐 The cause: Connection contamination
在事务池模式下,重置行为为了性能而被故意限制,所以会话状态可以保留,除非明确重置。如果客户端、脚本或自动化任务更改了会话级设置,该设置会“粘”在后端连接上。
🌐 In transaction pooling mode, reset behavior is intentionally limited for performance, so session state can persist unless explicitly reset. If a client, script, or automated task changes a session-level setting, that setting "sticks" to the backend connection.
当那个后端连接被归还到连接池时,下一个使用它的客户端会继承完全相同的状态。如果之前的脚本为了安全将连接设置为“只读”但没重置,之后任何应用尝试在同一个后端执行 UPDATE 或 INSERT 都会失败。
🌐 When that backend connection is returned to the pool, the next client to use it inherits that exact state. If a previous script set the connection to "read-only" for safety and failed to reset it, any subsequent application attempt to perform an UPDATE or INSERT using that same backend will fail.
为什么这个错误有时会发生? #
🌐 Why is the error sporadic?
这个错误看起来是间歇性的,因为它只在你的应用被随机分配到一个“受污染”的后台连接时才发生。同一连接池中的其他连接可能仍然处于默认的读写状态,这就导致成功请求和失败请求混在一起,让人迷惑。
🌐 The error appears intermittent because it only occurs when your application is randomly assigned a "contaminated" backend connection from the pool. Other connections in the same pool may still be in the default read-write state, leading to a confusing mix of successful and failed requests.
逐步解决 #
🌐 Step-by-step resolution
要解决这个问题,你必须找出并删除任何会全局修改会话状态而不是局部修改的命令。
🌐 To resolve this issue, you must identify and remove any commands that modify the session state globally rather than locally.
1. 审核应用和脚本 #
🌐 1. Audit application and scripts
在你的应用代码、迁移脚本和维护任务中搜索以下会话级命令:
🌐 Search your application code, migration scripts, and maintenance tasks for the following session-level commands:
SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY;SET default_transaction_read_only = on;
即使这些命令在辅助脚本中使用(比如数据导出或安全优先的维护任务),而不是在主应用中,它们仍然可能污染主应用使用的资源池。
🌐 Even if these commands are used in secondary scripts (like data exports or safety-first maintenance tasks) and not the main application, they can still contaminate the pool used by the main application.
2. 实现“安全”设置 #
🌐 2. Implement "safe" settings
如果你出于安全需要执行只读事务,可以使用只影响当前事务、不会在后端连接上保留的事务级命令。
🌐 If you need to execute a read-only transaction for safety, use transaction-level commands that only affect the current transaction and do not persist on the backend connection.
- 避免:
SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY;或SET default_transaction_read_only = on;(这些会污染连接池) - 使用方式:
BEGIN TRANSACTION READ ONLY;或BEGIN; SET TRANSACTION READ ONLY;(这些只影响当前交易)
如果你有现有的脚本使用会话级设置,并且不能立即重构的话:
🌐 If you have existing scripts that use session-level settings and cannot be immediately refactored:
- 临时解决方法: 确保他们在关闭与
SET default_transaction_read_only = off;的连接之前明确重置状态 - 最佳方法: 对于需要特殊会话状态的脚本,直接连接到5432端口(绕过连接池)
3. 连接字符串验证 #
🌐 3. Connection string verification
确保你的应用正在使用预期的连接池器。
🌐 Ensure your application is using the intended pooler.
- 共享交易池管理器 (Supavisor):
postgresql://postgres.PROJECT_REF:[YOUR-PASSWORD]@aws-X-REGION.pooler.supabase.com:6543/postgres - 专用事务连接池 (PgBouncer):
postgresql://postgres:[YOUR-PASSWORD]@db.PROJECT_REF.supabase.co:6543/postgres
交易池的最佳做法 #
🌐 Best practices for transaction pooling
- 使用专用连接进行维护: 如果脚本需要特定的会话状态(例如长时间运行的只读导出),请直接连接到数据库(端口 5432),而不是使用事务池(端口 6543)。这样可以防止维护设置泄漏到应用的连接池中。