Read-Only Is a Permission, Not a Capacity Limit: What Support Actually Gets When You Give Them Production Access
TL;DR: Granting read-only production access is often reviewed as a simple permissions change, but it is functionally an unmanaged capacity change. A read-only query cannot change data, but it can consume locks, disk space, and snapshot history, potentially triggering a production incident that standard access reviews are not designed to predict.
The request that gets answered by the wrong department
It is a familiar scene for any engineering leader: the Support or Product team is filing dozens of tickets a week for customer questions that can only be answered by a database query. Engineering becomes the bottleneck, response times slip, and someone proposes a logical solution: "Just give the Support Lead read-only production access so they can unblock themselves."
The request is usually routed to whoever owns IAM or database permissions. The review focuses on security—ensuring the credentials cannot DROP TABLE or UPDATE records. Once the "read-only" constraint is verified, the access is granted, and the risk is considered handled.
However, this framing is fundamentally flawed. Read-only is a constraint on what a query can write; it is not a constraint on what a query can consume. When you grant this access, the organization is making a significant capacity decision through a permissions process, often without the guardrails necessary to protect production stability.
What a read actually costs
While a SELECT statement appears passive, it consumes shared resources that every other production process relies on. According to the PostgreSQL documentation, there are four primary ways a read-only query can cause production harm, and in most default configurations, the guardrails for these risks ship disabled.
- Locks. A
SELECTstatement acquires anACCESS SHARElock. According to PostgreSQL documentation, "Once acquired, a lock is normally held until the end of the transaction." If an analyst runs a long-running report, they hold that lock. If an automated migration arrives and requires an exclusive lock, it will wait behind the analyst. Crucially, that pending exclusive lock then blocks all subsequent reads, stalling the entire application. (See our companion piece on how ALTER rewrites block unreadable tables for the full queue mechanism). - Disk. When queries involve large sorts or hashes that exceed
work_mem(which defaults to a modest 4MB), PostgreSQL "writ[es] to temporary disk files." The guardrail for this istemp_file_limit. According to PostgreSQL resource documentation, the default is -1, meaning "no limit." Since this limit is per-process, five concurrent support queries could theoretically consume all available disk space, and becauselog_temp_filesdefaults to off, the failure is often invisible until the volume is full. - Cleanup. An open read transaction pins the
xminhorizon, preventingVACUUMfrom removing dead rows that might still be visible to that transaction. This leads to table bloat and performance degradation. As noted in the documentation for idle_in_transaction_session_timeout, even a transaction holding no significant locks can contribute to bloat if it remains idle. This is particularly dangerous because a read-only transaction has a nullbackend_xidbut a livebackend_xmin, meaning monitoring tools looking for long "write" transactions will miss it entirely. - Replication. This is often the most misunderstood cost, as many teams assume moving queries to a replica eliminates the risk.
The replica is a trade, not an escape
Moving analytical queries to a read replica does not remove the trade-off; it simply relocates it. According to vendor documentation from AWS, Google, and Microsoft, there is no "free" way to handle long-running reads on a standby.
If you leave hot_standby_feedback at its default of off, the read replica will forcibly cancel queries that conflict with incoming WAL records from the primary. As Azure documentation explains, a VACUUM operation on the primary can cause a query cancellation on the replica if it removes rows the replica still needs.
If you turn hot_standby_feedback on to protect the queries, you stop the cancellations but trigger bloat on the primary server. AWS documentation warns that this "prevents autovacuum on the writer instance from removing dead rows." On Aurora PostgreSQL, this trade is often made for you, as the setting is enabled by default and unmodifiable.
Google Cloud SQL provides the most honest structural answer: you may need separate replicas for OLTP (transactions) and OLAP (analytics) to prevent long-running queries from blocking essential replication.
One claim that does not survive checking
It is a common intuition among engineers that a large sequential scan will "blow out" the buffer cache, evicting frequently used data. However, the PostgreSQL source code shows this is largely mitigated. According to the Postgres storage README, the engine uses a 256KB "ring buffer" (BAS_BULKREAD) for large scans specifically to avoid flushing the entire cache. The caveat is narrow: this only engages for relations exceeding a quarter of shared_buffers and does not apply to index scans. We mention this because a serious look at production access requires distinguishing between theoretical fears and documented mechanical risks.
Three options, all of them mispriced
When a non-engineering team needs production data, organizations usually choose one of three paths, and all are commonly mispriced:
- Escalate everything to engineering: The price isn't the query; it’s the interrupt and the queue. This is a recurring tax paid by your most expensive headcount. As we've noted in our Twelve Escalation Metrics discussion, these interrupts are often unmeasured and unmanaged.
- Grant read-only access: This is priced as a simple permission change. In reality, it is a capacity grant with no owner, no telemetry, and no bounds. It works perfectly until the first long report brings production to its knees.
- Build a purpose-shaped path: Whether it’s a dedicated OLAP replica or a bounded query interface, this is priced as a "project nobody wants to staff." Yet, it is the only option where the cost is known in advance and the risks are bounded.
Key Takeaway: Read-only credentials manage what a user can change, but only a capacity envelope can manage what a user can break.
What an honest "Yes" looks like
If you decide to grant production access, the deliverable should not be a username and password. It should be a capacity envelope. An honest "yes" includes:
- A separate role: Do not share credentials.
- A separate connection pool: Ensure a surge in support queries cannot exhaust the primary application’s connections.
statement_timeoutandtemp_file_limit: Set these specifically on the analyst role to kill runaway queries before they impact others.idle_in_transaction_session_timeout: Set this to bound the age of snapshots and prevent vacuum blocking.log_temp_files: Enable this so you can see large disk usage in your logs before the disk fills up.- A dedicated replica: If the workload is analytical, follow Google's guidance and isolate it from the streaming replication used for high availability.
Notice that every item in this list is a configuration setting, not a permission. Not one of these would surface in a standard security access review.
The question underneath the request
Ultimately, Support did not want database access. They wanted an answer without waiting for a person. Access is merely one way to get that answer, and it is the way that transfers unbounded operational risk to a team with no way to see it coming.
The alternative—building a controlled, observable path—is not free. But the choice is between a cost you can measure and an incident you cannot predict.
Why this matters
As organizations scale, the ratio of support and product staff to engineering staff grows. This creates a compounding pressure to grant direct production access to "unblock" the business. Without a clear understanding that read-only access is a capacity risk, teams will continue to create availability hazards in the name of organizational efficiency.
Sources & further reading
- According to PostgreSQL docs,
temp_file_limitdefaults to -1 (no limit). - According to AWS RDS documentation, using per-role settings for timeouts is a best practice for analytics sessions.
- According to Google Cloud SQL, separate replicas should be used for OLTP and OLAP workloads to avoid replication lag.
- According to MySQL documentation, history list growth occurs even for read-only transactions under Repeatable Read isolation.
At Operate, we build a platform that watches production and investigates incidents autonomously. Because Operate reads production data to find root causes, we are deeply aware of the trade-offs discussed here. The underlying request—getting an answer without waiting for a human—is the same whether it comes from a support lead or an AI agent. The difference lies in whether that path is bounded and observable. Operate is self-hosted and read-only with a full audit trail, but more importantly, it is designed to propose a fix as a PR for human review rather than acting directly on production, ensuring the "human in the loop" remains the ultimate guardrail for capacity and correctness alike.