phacility/phabricator · critical · Exception

Attempting to issue a write query on a read-only connection

Error message

Attempting to issue a write query on a read-only connection (to database "%s")!

What it means

Thrown by AphrontBaseMySQLDatabaseConnection::checkWrite() when a statement classified as a write (anything not starting with SELECT/SHOW/EXPLAIN, after optional leading parens) is issued on a connection opened in read-only mode. Phabricator marks replica connections read-only; this guard makes accidental writes to a replica loud and immediate instead of silently corrupting data. On writes to writable connections it also fires AphrontWriteGuard::willWrite() for write-safety checking.

Source

Thrown at src/infrastructure/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php:299

    } else if (is_bool($result)) {
      return $this->getAffectedRows();
    }
    $rows = array();
    while (($row = $this->fetchAssoc($result))) {
      $rows[] = $row;
    }
    $this->freeResult($result);
    return $rows;
  }

  protected function checkWrite($raw_query) {
    // NOTE: The opening "(" allows queries in the form of:
    //
    //   (SELECT ...) UNION (SELECT ...)
    $is_write = !preg_match('/^[(]*(SELECT|SHOW|EXPLAIN)\s/', $raw_query);
    if ($is_write) {
      if ($this->getReadOnly()) {
        throw new Exception(
          pht(
            'Attempting to issue a write query on a read-only '.
            'connection (to database "%s")!',
            $this->getConfiguration('database')));
      }
      AphrontWriteGuard::willWrite();
      return true;
    }

    return false;
  }

  protected function throwQueryException($connection) {
    if ($this->nextError) {
      $errno = $this->nextError;
      $error = pht('Simulated error.');
      $this->nextError = null;
    } else {

View on GitHub (pinned to 5720a38cfe)

Solutions

  1. Route the write to the master/writable connection (open a new connection without read-only, or fix the cluster role configuration)
  2. Check bin/config get cluster.databases / partition configuration: exactly one host must be writable
  3. In application code, never save Lisk objects on connections used for replica reads — re-load/save via the default connection
  4. During/after a masterswitch, let connections reconnect so roles refresh

Example fix

// before: writing on whatever connection is at hand
$conn = $this->getAnyDatabaseConnection();
queryfx($conn, 'UPDATE %T SET status = %s WHERE id = %d', ...);

// after: writes go to the writable master connection
$conn_w = id(new PhabricatorUser())->establishConnection('w');
queryfx($conn_w, 'UPDATE %T SET status = %s WHERE id = %d', ...);
Defensive patterns

Strategy: validation

Validate before calling

// Before writing, confirm the connection is writable.
if ($conn->getReadOnly()) {
  $conn = $lisk_object->establishConnection('w'); // master connection
}
queryfx($conn, 'UPDATE ...');

Try / catch

try {
  queryfx($conn, $write_sql);
} catch (Exception $ex) {
  if (preg_match('/read-only connection/', $ex->getMessage())) {
    $conn = $lisk_object->establishConnection('w');
    queryfx($conn, $write_sql); // retry once on the writable connection
  } else {
    throw $ex;
  }
}

Prevention

When it happens

Trigger: Issuing execute() with INSERT/UPDATE/DELETE/CREATE through a connection obtained from a replica (getManagementConnectionData / partition or cluster configuration giving a read-only connection); daemons or lisk objects that lazily write while iterating objects loaded from a read-only connection; a host in 'replica' role receiving writes during a cluster masterswitch.

Common situations: Misconfigured cluster.databases marking the primary as a replica; code paths reading from a replica and then saving a Lisk object on the same connection; during masterswitch/failover the writable role moves while a request still holds an old read-only connection.

Related errors


AI-assisted analysis of phacility/phabricator@5720a38cfe (2026-08-21). Data as JSON: /api/errors/a06374df40cc1468. Report an issue: GitHub.