Skip to main content

DEPLOY.CONTEXT.READ_ONLY_TARGET — The instance this deploy points at will not accept writes

  • Category: safety
  • Severity: high
  • Level: 0
  • Downtime class: blocking — the catalog names it as a fixed value, and the finding this check emits carries it. A deploy aimed at an instance that refuses writes stops where it stands
  • Stability: stable
  • Suites: deploy
  • Applies to: PostgreSQL and MySQL

What it reports

The connection this run addresses reaches an instance that refuses writes. Two questions per engine, both answered from what the server publishes about itself.

PostgreSQL — one statement, two columns:

select pg_is_in_recovery() as in_recovery,
current_setting('default_transaction_read_only') as read_only

Recovery is answered first. When pg_is_in_recovery() comes back as 1, t, true or on, the finding names that function and calls the target a standby — this instance replays from a primary and accepts no writes of its own. Only when the instance is not in recovery is default_transaction_read_only consulted; at on, true or 1 the finding names that setting and calls the target a primary configured to refuse writes.

MySQL — one statement, two variables:

select @@super_read_only as super_read_only, @@read_only as read_only

super_read_only is answered first. At 1 the finding names it and says the server is refusing writes from every account, including ones holding SUPER. Otherwise read_only at 1 is the finding, described as refusing writes from accounts without SUPER — which the migration account may or may not hold.

Either way, exactly one variable is named, the connection is named with it, and the finding is located on that setting:

The connection "…" reaches a standby — this instance replays from a primary and accepts no writes of its own (pg_is_in_recovery()). A migrate --force sent here is refused by the server, so this deploy fails at the migration step rather than at this gate — which is the same outcome with none of the warning. Point the deploy at the primary, or clear the setting before it runs.

When neither question answers yes, the check passes.

Why the check never tries a write

The obvious way to answer will writes be accepted is to attempt one and roll it back. This package will not, and the reason is the promise rather than the risk: a preflight that writes has written, whatever it did afterwards. A rolled-back transaction still takes locks, still burns an xid, and still makes the sentence this tool never writes to your database false. So the state is read, through the run's own read-only session.

Why it is High rather than a warning

Every other context check says a migration might go badly. This one says the next command will be refused: migrate --force against a standby or a read-only instance is rejected outright. The gate ran, said nothing, and the deploy broke two lines later on something the gate could see.

That is a different kind of statement, and the severity follows from certainty rather than from degree. It is also why the check sits second in the deploy run, right behind the version check: whether the instance can accept writes at all decides whether the deploy can happen — everything after it is about how badly it might go.

What to do about it

Read which of the two the finding named — that is the decision, and the report already made it for you. A standby and a deliberately read-only primary have the same consequence and completely different fixes, and a report that merged them would send you to the wrong place.

  • pg_is_in_recovery() — the connection is pointed at the wrong host. Nothing on that machine is worth changing; a standby refuses writes because that is what a standby is. Point the deploy at the primary.
  • default_transaction_read_only — this is a primary, and somebody chose the setting. Clear it before the deploy runs, or point the deploy at whichever instance is meant to take writes.
  • super_read_only — the stronger MySQL flag, and it implies the weaker one, so it is the one actually holding the door shut. That is why the finding names it when both are on: pointed at read_only instead, an operator would be looking at the flag that is not the reason.
  • read_only — writes are refused for accounts without SUPER. Whether that includes the account your migrations run as is a question about that account, not about this flag.

When it cannot answer

If the read fails, the result is undetermined with the reason attached — never a pass:

the server would not say whether it accepts writes (…). Nothing follows from that: an unread setting is not a permissive one, and a deploy sent at a standby fails whether or not this gate could see it coming.

sqlens:predeploy is fail-closed, so an undetermined check blocks the deploy; --allow-undetermined is the deliberate way past it. See Understanding undetermined.

What it does not claim

  • It reads the target's own view of itself. A connection routed to a replica by a pooler or a proxy reports what that replica knows, and the routing layer is invisible here. If your connection configures a read/write split, the read/write-split page is the one that covers which server is being addressed at all.
  • It says the target cannot accept writes, not that the deploy is wrong. Pointing a migration at a replica is usually a configuration mistake, and this check names it rather than deciding it.
  • A value it cannot interpret is read as "not on", not as a verdict. A column that does not come back as a scalar reads as not read-only everywhere above, which is the safe direction here: a false alarm at a deploy gate is how the gate gets configured away. A genuinely unreadable state throws instead and comes back undetermined.
  • Only one variable is named per finding. On PostgreSQL a standby short-circuits before default_transaction_read_only is looked at; on MySQL read_only is not named while super_read_only is on. The one named is the one to act on, not the only one that may be set.