Answer
Enabling readonly_mode = true blocks all write operations that originate from Grafana’s HTTP API or UI (POST, PUT, PATCH, DELETE) and returns HTTP 403 Forbidden for those requests. It does not prevent direct database writes or internal write attempts by some data‑source plugins. While the mode is active, Grafana continues to serve GET requests (dashboard rendering, metric queries, alert rule evaluation, provisioning reads, etc.) without any additional restriction; the number of concurrent read requests is limited only by the server’s HTTP capacity and available resources, not by the read‑only flag itself.
Confirmed facts
- Read‑only mode is activated by setting
[security] read_only = true in grafana.ini or the environment variable GF_SECURITY_READ_ONLY=true.
- When active, Grafana logs
logger=security level=info msg="Read‑only mode enabled" on startup.
- Any POST/PUT/PATCH/DELETE to the Grafana HTTP API (e.g.,
POST /api/dashboards/db) receives a 403 response with JSON {"message":"Read‑only mode is enabled"}.
- GET requests to existing resources (dashboards, data sources, alert rules, etc.) continue to return 200 OK and the expected data.
- Alert rule evaluation works because it only reads rule definitions and queries data sources; creating/updating/deleting alert rules via API/UI is blocked.
- Provisioning files are processed only at Grafana start‑up; enabling read‑only mode after start‑up does not retroactively apply provisioning changes until a restart.
Likely explanation
The read‑only flag is a security layer that intercepts mutating HTTP routes before they reach the business logic. It does not wrap direct database access, so a user with DB credentials could still alter tables, and certain plugins that internally issue WRITE‑like calls (e.g., writing annotations to a backend cache) will see those calls fail with 403 errors logged in Grafana.
Steps to verify for your migration
- Confirm the mode is active:
grep "Read‑only mode enabled" /var/log/grafana/grafana.log (or check stdout if running in container).
- Test a blocked write:
curl -s -o /dev/null -w "%{http_code}" -X POST http://localhost:3000/api/dashboards/db -H "Content-Type: application/json" -d '{"dashboard":{}}' should return 403.
- Test an allowed read:
curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/api/dashboards/uid/ should return 200 and the dashboard JSON.
- Monitor concurrent GET traffic during migration (e.g., with
grafana-cli admin proxy or your load‑balancer metrics) to ensure the server’s server.max_concurrent_connections or worker pool size meets your expected read load.
Missing diagnostic detail
If you need to guarantee a specific maximum number of concurrent read requests (e.g., a target QPS during migration), please provide the expected read traffic volume or the hardware/container limits you are working with; that information will determine whether you need to adjust server.max_concurrent_connections or increase pod/replica resources.