General
Your AG Secondaries Aren't Load Balancing Because You Skipped One Step
Arvind Toorpu DEV Community 周榜
2 views
A client paged me at 6am because their reporting dashboards were timing out. Nothing was down. The primary replica in their Always On AG was just getting hammered by every single read query in the environment, while two perfectly healthy secondary replicas sat there doing nothing but redo. They had "configured" read-scale months earlier: ALLOW_CONNECTIONS = READ_ONLY was set on both secondaries, the app connection string had ApplicationIntent=ReadOnly, and none of it mattered because nobody had built the read-only routing list on the primary. Connections with read intent were falling through to the primary because that's what happens by default when a routing list doesn't exist.
That's the whole point of this post. Making a secondary readable and getting traffic to actually route to it are two different configuration steps, and skipping the second one is the single most common mistake I see in AG read-scale setups.
What read-only routing actually is
Read-only routing is a primary-replica-side redirect. A client connects to the listener (or, in a clusterless read-scale AG, to a replica directly) with ApplicationIntent=ReadOnly in the connection string. If the instance it lands on is the primary, SQL Server checks whether a read-only routing list exists for that primary. If one does, it transparently redirects the connection to whichever secondary the list points to. No list, no redirect, the connection just runs read-write on the primary like normal.
Since SQL Server 2017 you don't even need a Windows Server Failover Cluster to get this: a read-scale availability group can exist without a cluster at all, purely to fan out read workload across replicas (Microsoft Learn). Be clear with yourself and your team about what that buys you, though: a cluster-less read-scale AG has no automatic failover. There's no cluster manager watching health and flipping the primary role for you. If you need that, you're building a normal Always On AG with WSFC (or Pacemaker on Linux) and layering read-only routing on top of it, which is the far more common production pattern.
Configuring it end to end
Here's the sequence that actually works, on a standard Windows-clustered AG with two readable secondaries.
First, mark each secondary as readable and give it a routing URL:
ALTER AVAILABILITY GROUP [AG1]
MODIFY REPLICA ON N'SQLNODE02'
WITH (SECONDARY_ROLE (ALLOW_CONNECTIONS = READ_ONLY,
READ_ONLY_ROUTING_URL = N'TCP://SQLNODE02.corp.local:1433'));
ALTER AVAILABILITY GROUP [AG1]
MODIFY REPLICA ON N'SQLNODE03'
WITH (SECONDARY_ROLE (ALLOW_CONNECTIONS = READ_ONLY,
READ_ONLY_ROUTING_URL = N'TCP://SQLNODE03.corp.local:1433'));
This is the step people actually remember. Here's the one they don't: the routing list has to be defined on whichever replica is currently the primary, because that's the instance doing the redirecting.
ALTER AVAILABILITY GROUP [AG1]
MODIFY REPLICA ON N'SQLNODE01'
WITH (PRIMARY_ROLE (READ_ONLY_ROUTING_LIST = (('SQLNODE02','SQLNODE03'), 'SQLNODE01')));
That nested-parentheses syntax, (('SQLNODE02','SQLNODE03'), 'SQLNODE01'), is load-balanced round-robin routing across SQLNODE02 and SQLNODE03, with SQLNODE01 (the primary) as the fallback if both are unavailable. That's a SQL Server 2016+ feature; only one level of nesting is supported (Microsoft Learn).
Because any node can become primary during a failover, you need to repeat that PRIMARY_ROLE routing list definition on every replica, each one listing itself last as the fallback:
ALTER AVAILABILITY GROUP [AG1]
MODIFY REPLICA ON N'SQLNODE02'
WITH (PRIMARY_ROLE (READ_ONLY_ROUTING_LIST = (('SQLNODE01','SQLNODE03'), 'SQLNODE02')));
ALTER AVAILABILITY GROUP [AG1]
MODIFY REPLICA ON N'SQLNODE03'
WITH (PRIMARY_ROLE (READ_ONLY_ROUTING_LIST = (('SQLNODE01','SQLNODE02'), 'SQLNODE03')));
On the client side, the connection string needs the intent flag and, if the listener spans subnets, multi-subnet failover:
Server=tcp:AG1-Listener,1433;Database=Sales;Application Intent=ReadOnly;MultiSubnetFailover=True;
For ad hoc testing from SSMS or sqlcmd, you can confirm which node you actually landed on:
SELECT @@SERVERNAME AS routed_to, sys.fn_hadr_is_primary_replica('Sales') AS is_primary;
And to check what's actually registered on the primary before you blame the app:
SELECT ag.name AS ag_name, ar.replica_server_name, rl.read_only_routing_url
FROM sys.availability_read_only_routing_lists rl
JOIN sys.availability_replicas ar ON rl.replica_id = ar.replica_id
JOIN sys.availability_groups ag ON ar.group_id = ag.group_id;
That query would have caught my client's problem in about ten seconds.
Where this requires care
The primary needs a routing URL too. If the routing list ever falls back to the primary (or a client connects to the primary directly with read intent and no list exists yet), and the primary itself has no READ_ONLY_ROUTING_URL, routing fails outright rather than silently running on the primary.
Load-balanced routing is round-robin per new connection, not per query. It's not smart about replica lag or current load. If SQLNODE03 is 40 seconds behind on redo because of a big index rebuild, round-robin will happily send a client there and that client will see stale data with no warning. If your reporting workload is lag-sensitive, monitor sys.dm_hadr_database_replica_states.secondary_lag_seconds and either alert on it or take that replica out of the routing list temporarily during heavy write windows.
Readable secondaries are not free connections. They're separate SQL Server instances and, outside a passive failover-only configuration, they need to be licensed like any other active SQL Server. This trips up teams who treat "we already pay for AG" as covering read-scale for free. Check your licensing terms, don't assume.
Basic Availability Groups on Standard Edition don't support readable secondaries at all. If you're on Standard Edition, you get one AG, one database, no read access to the secondary, full stop. Read-scale in the way this article describes needs Enterprise Edition (or SQL Server 2017+ read-scale-without-a-cluster, which has its own edition rules, worth double-checking against your specific version).
Application Intent is case-sensitive in spelling but the driver matters more than you'd think. Some older ODBC/JDBC driver versions silently drop or mishandle ApplicationIntent. If routing isn't happening and your T-SQL config checks out clean, check the actual driver version and connection string the app is sending, not just what you think you configured.
My take
Read-only routing is one of those Always On features that looks like a checkbox in the wizard and is actually two independent, un-obviously-linked configuration objects: the secondary's readability plus URL, and the primary's routing list. SSMS's Add Replica wizard doesn't force you through the second one clearly, and I've seen more environments get half-configured than fully configured. If you're rolling this out, script both steps together, verify with the DMV query above as part of your deployment, and don't trust the dashboard alone to tell you traffic is actually landing where you think it is.
Further reading
Configure read-only routing for an availability group
Use read-scale with Always On availability groups
sys.availability_read_only_routing_lists (Transact-SQL)
Configure a Read-Scale Availability Group (SQL Server on Linux)
Read original: https://dev.to/arvind_toorpu/your-ag-secondaries-arent-load-balancing-because-you-skipped-one-step-3bda
← Previous
🧟🤖 How I Led an AI-Native Cleanup of 120K+ dormant SaaS Tenants & Zero Customer Impact
Next →
way to search for keywords on a page of links
Related
Construí un buscador de disponibilidad para las bibliotecas del metro de Madrid (y un sistema para verificar que no se rompe)
General
0
DEV Community
🧟🤖 How I Led an AI-Native Cleanup of 120K+ dormant SaaS Tenants & Zero Customer Impact
General
1
DEV Community 周榜
Building an OS from scratch is a different kind of challenge.
General
1
DEV Community 周榜
Material Design 4 in Android: UX Patterns That Convert
General
3
DEV Community 周榜
Comments0
No comments yet — be the first