PlanetScale — Introducing PlanetScale Portals: Read-only regions¶
Summary¶
Taylor Barnett (PlanetScale, 2022-05-24) launches PlanetScale Portals, a managed feature that lets a single PlanetScale MySQL database add arbitrarily many read-only regions — each a full replica of the primary-region dataset, sitting in a region close to the application tier. Each database still has a single writing region (all writes go there); reads can now be served from any of the replica regions, cutting per-query read latency from "cross-ocean one-way + queue + return" down to "in-region one-way + queue + return". The post makes the launch concrete with a worked Ruby-on-Rails integration built on Rails' multiple-databases / automatic role switching machinery, and canonicalises the session-cookie read-your-writes window — the trick of setting a "reads-go-to-primary for the next N seconds after a write" cookie so users never read stale data they just wrote, even though the global read pool is eventually consistent.
Key takeaways¶
- Portals = N read-only regions off one writing region. "Today, each database in PlanetScale reads and writes from a single region. But with Portals, you can add as many distributed read-only regions as you want." Writes remain single-region (MySQL / Vitess semi-sync, all the familiar constraints); reads fan out. This is the canonical multi-region managed-MySQL shape: one primary region
-
many regional read replicas, surfaced as per-region MySQL endpoints.
-
Quantified latency benefit: ~90 ms → ~3 ms per query, Frankfurt app talking to Northern-Virginia vs Frankfurt DB. "For an application deployed to Frankfurt, talking to a Northern Virginia database can add nearly an extra ~90ms PER query. By adding a read-only region in Frankfurt, we can reduce that to ~3ms per query." (Measurement:
select * from books limit 10.) The 30× improvement is the full edge-to-origin database latency delta — each query skips a transatlantic RTT. For a page that issues N queries, latency savings multiply by N before any app logic runs. Canonical motivating number for the per-region read-replica routing pattern. -
Per-region endpoint = separate connection string per replica region. "You connect to your new read-only regions with a connection string, just like connecting to any other PlanetScale database branch or other MySQL databases. This connection string is specific to both your read-only region and production branch." The app side sees a regular MySQL host; the routing decision (which region's hostname to dial) is pushed to the application via an env-var lookup (
region = ENV["APP_REGION"]) keyed to the region the app pod is currently running in. This is the 2022-era per-region-credential model; the 2024 global replica credentials launch later collapses the N-per-region endpoints into a single global credential that routes to the lowest-latency replica per-query via PlanetScale Global Network. -
Rails multiple-databases +
primary_replicais the idiomatic integration. The post walks through Rails' standard multiple-databases setup: declareprimary+primary_replicaconnections indatabase.yml, markprimary_replica: replica: trueso Rails knows it's read-only, then wrap read-heavy blocks inActiveRecord::Base.connected_to(role: :reading) do … endfor manual routing, or opt into automatic role switching by callingconnects_to database: { writing: :primary, reading: :primary_replica }inApplicationRecordand enablingActiveRecord::Middleware::DatabaseSelectorin production config. Default reads then route to the replica pool; writes still go to primary. (Code thanks to Mike Coutermarsh in the post's own acknowledgments — same PlanetScale-Rails voice as the FastPage and SQLCommenter posts.) -
Canonical read-your-writes trick: session cookie pins reads to primary for N seconds post-write. "we can also tell Rails to read from our primary if the user recently wrote to the database. This protects our users from ever reading stale data due to replication lag." Configured via:
config.active_record.database_selector = { delay: 2.seconds }
config.active_record.database_resolver =
ActiveRecord::Middleware::DatabaseSelector::Resolver
config.active_record.database_resolver_context =
ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
"After each write, it will set a cookie that will send all reads to the primary for 2 seconds, allowing users to read their own writes." This is a concrete instance of session-cookie read-your-writes: sacrifice in-region read latency for this one user for the next N seconds in exchange for defeating replication-lag-visible staleness. The 2-second window must exceed the p99 cross-region lag, or users still see ghost-writes.
-
Fly.io cross-ref positions Portals as the DB-side half of a global-app stack. "read the Fly.io and PlanetScale guide on using Fly's Global Application Platform alongside PlanetScale's read-only regions to deploy database regions close to your applications." The rhetoric — app platform moves compute close to users, database platform moves read replicas close to compute — is the canonical 2022-era pairing and the reason Portals existed at all: Fly (and peers) made it cheap to run app pods in 20+ regions, which exposed the cross-ocean-per-query-RTT tax as the new bottleneck. See companies/flyio + Fly Machines for the compute-side half.
-
Pricing: storage × (1 + N replica regions), plus row-reads per region. "Portals' storage costs are prorated by month … Your storage costs will increase linearly with the number of read-only regions you purchase. For example, if your production branch is 10GB, each read-only region added will increase your total storage cost by 10GB … Queries issued to your read-only region will contribute to your total billable row reads per month." Pricing dimension is honest about the architecture — each replica region is a full dataset copy, so storage scales linearly with region count, and the billing line separates per-region row-reads so the operator can see which regions are pulling their weight. Available on Base or Enterprise plans.
Architectural numbers¶
- Cross-region single-query latency (Frankfurt-app → NoVa-DB): ~90 ms baseline.
- In-region single-query latency (Frankfurt-app → Frankfurt- replica via Portals): ~3 ms.
- Measured query:
select * from books limit 10(trivially cheap on the DB — the 90 ms is essentially pure RTT + connection-handling; a more complex query dominated by DB work would see a smaller relative benefit but the same absolute ~87 ms saved per round-trip). - Session-cookie RYW window in post's example:
delay: 2.seconds— configurable; must exceed p99 replication lag.
What Portals is not¶
-
Not multi-primary. Writes still go to one region. Portals is read-scaling + read-latency, not write-scaling. (Write-scaling is the shard/Vitess story — Vitess / sharding — a different dimension of the problem.)
-
Not strongly consistent across regions. Reads to a replica region see data as of that replica's current replication-lag. The session cookie trick makes the user's own writes visible to them immediately (by steering them to primary for 2 s), but it does not make user-A's write visible to user-B in region X any faster than replication allows.
-
Not a CDN. It's a MySQL-endpoint-per-region model. The CDN- shaped connection-termination layer — PlanetScale Global Network — came later (2024), collapsing the per-region endpoint model into a single global credential that routes per-query over warm mesh connections.
Caveats & limits¶
-
Product launch + customer-tutorial slant. ~30% of the post is launch-announcement + beta-availability + pricing; ~50% is the Rails code walkthrough. The architecturally-dense content — the 90 ms → 3 ms latency measurement, the session-cookie read-your-writes mechanism, the per-region credential model, the role-switching mechanism — lives in the other ~20%, alongside sufficient motivation that it carries the post over the companies/planetscale Tier-3 scope bar.
-
Rails-only worked code. The mechanism (per-region connection string + read-write split + session-cookie RYW window) is framework-agnostic; the concrete code is Rails-specific. Django (
DATABASE_ROUTERS), Laravel (DB::connection('read')), and plainmysql2-driver apps can implement the identical shape. -
Does not show how Portals replicates internally. The post doesn't say whether Portals uses MySQL native async replication, Vitess's VReplication, or a cross-region tunnel — only describes the experience. Later PlanetScale posts confirm the Vitess-replication substrate.
-
~2022 baseline. Predates the Global Network launch; the per-region-credential model is the thing the 2024 global-replica- credential launch replaced. Post is still the best public reference for why regional read replicas matter and the session-cookie RYW idiom, but the "you connect with a region-specific connection string" mechanism is obsolete inside PlanetScale's current product.
Cross-refs¶
Systems introduced or materially extended
- systems/planetscale-portals — new. PlanetScale's managed- read-only-region feature; succeeded at the connection-model layer by systems/planetscale-global-network in 2024 but still the canonical read-region replication feature.
- systems/planetscale — extended (2022-era "Recent articles" entry + Portals call-out in system-page body).
- systems/ruby-on-rails — extended (Rails multi-database + automatic role switching as the canonical application-side integration).
Concepts introduced
- concepts/read-your-writes-consistency — new. The consistency model violated by naive read-replica routing; Portals post is one of the canonical sources.
- concepts/regional-read-replica — new. Generalises the "replica in region X of primary in region Y" shape beyond PlanetScale.
- concepts/session-cookie-read-your-writes-window — new. The specific trick of using a session cookie to pin a user's reads to primary for N seconds post-write.
Concepts extended
- concepts/replication-lag — new "seen in — session-cookie RYW window as an operational upper bound" cross-ref added.
- concepts/edge-to-origin-database-latency — explicit 90 ms → 3 ms motivating number.
- concepts/read-write-splitting — Rails automatic role switching as canonical framework-level implementation.
Patterns introduced
- patterns/session-cookie-for-read-your-writes — new. The 2-second-delay cookie as read-your-writes-preserving mechanism.
- patterns/per-region-read-replica-routing — new. Application chooses nearest replica region via env-var keyed to app deployment region.
Patterns extended
- patterns/read-replicas-for-read-scaling — extended to cover the multi-region deployment shape (not just same-region read scaling).
Source¶
- Original: https://planetscale.com/blog/introducing-planetscale-portals-read-only-regions
- Raw markdown:
raw/planetscale/2026-04-21-introducing-planetscale-portals-read-only-regions-6b2f7c3b.md
Related¶
- companies/planetscale
- systems/planetscale, systems/planetscale-portals, systems/planetscale-global-network, systems/vitess, systems/mysql, systems/ruby-on-rails
- concepts/read-your-writes-consistency, concepts/regional-read-replica, concepts/session-cookie-read-your-writes-window, concepts/replication-lag, concepts/edge-to-origin-database-latency, concepts/read-write-splitting
- patterns/session-cookie-for-read-your-writes, patterns/per-region-read-replica-routing, patterns/read-replicas-for-read-scaling