Aurora Writer vs Reader Endpoint: Which One to Use
Use the cluster (writer) endpoint for anything that writes, and the reader endpoint for read-only traffic. The writer endpoint (Aurora calls it the "cluster endpoint") always points at whichever instance is currently primary, and it's the only one you can send an INSERT, UPDATE, or DDL statement to. The reader endpoint spreads new connections across your Aurora Replicas — but here's the part almost nobody's documentation says out loud: if your cluster has zero replicas, the reader endpoint quietly connects you to the writer instance anyway, and lets you write through it. That single behavior is responsible for more "why did my read-only endpoint just accept a write" support tickets than every other Aurora endpoint quirk combined.
Jake's shop runs a little inventory app on top of an Aurora MySQL cluster — one writer, two replicas, nothing exotic. Two weeks ago a Saturday-afternoon stock sync started throwing errors that made no sense: "The MySQL server is running with the --read-only option so it cannot execute this statement." Nothing in the code had changed since Tuesday.
"I didn't touch the connection string," Jake said. "I copy-pasted it out of the console like always." He had — except the console shows you four different endpoint boxes on that page, and at some point over the previous month, somebody on his two-person team had grabbed the reader endpoint instead of the writer endpoint while "fixing" a timeout. It worked fine for weeks, because most of what the sync job did was reads. Then it hit its one write, on a Saturday, at the worst possible moment for a phone shop that takes trade-ins on weekends.
The one question that ends 90% of this confusion
Before you touch a connection string, ask exactly one thing: does this specific piece of code ever, under any circumstance, write to the database? Not "mostly reads." Not "reads 95% of the time." Ever, at all, even once, even in an edge case someone added eight months ago.
If the answer is yes — even a maybe — that code needs the cluster endpoint. Full stop. The reader endpoint is not a "read-mostly, write-occasionally-and-it'll-figure-it-out" endpoint. On a healthy cluster with replicas attached, it is a strictly read-only door, and Aurora will reject any write attempted through it. The confusion nearly everyone runs into is that this rule has one exception, and the exception is silent.
🙋♂️ Jake's Reality Check
"Why did it work for a month and then suddenly break? If it was the wrong endpoint, shouldn't it have failed on day one?"
Because Aurora's reader endpoint only refuses writes when the cluster actually has read replicas attached to balance across. If a cluster is ever running with just the primary instance and nothing else — during a maintenance window, a scaled-down dev environment, or a replica that's temporarily gone — the reader endpoint connects you straight to that primary, and it will happily accept writes through it. Nothing errors. Nothing warns you. It just works, right up until a replica comes back online, at which point the exact same code starts failing.
Ethan calls this "the endpoint that lies by omission." "It's not broken behavior — it's documented — but it means testing against a single-instance dev cluster proves nothing about whether your read/write split is correct. You have to test it against a cluster that actually has a replica, or you're not testing the thing you think you're testing."
What each Aurora endpoint actually does
An Aurora DB cluster is not one server — it's a group of DB instances sharing one storage layer. Every connection has to land on a specific instance somehow, and rather than making you track which instance is doing what, Aurora hands you a set of DNS names, called endpoints, that route you to the right place automatically. There are four you'll deal with regularly, plus a fifth that only matters if you run Aurora Global Database.
| Endpoint | How many per cluster | Points to | Use it for |
|---|---|---|---|
| Cluster (writer) | Exactly one | The current primary instance, always | Writes, DDL, DML, ETL, transactions, general app traffic |
| Reader | Exactly one | One of the Aurora Replicas, connection-balanced (or the primary if there are none) | SELECT-only workloads: reporting, analytics, read-heavy pages |
| Instance | One per instance in the cluster | That exact instance, and only that one, permanently | Diagnosing or tuning one specific box; not for app traffic |
| Custom | As many as you create | A subset of instances you define | Splitting workloads across instances of different sizes |
The cluster endpoint — often called the "writer endpoint"
It is one the cluster endpoint, but most engineers just say "writer endpoint," and either name is fine because they mean the same thing. It's the address you use to develop and test applications and to run write operations — INSERT statements, DDL (the commands that create or change tables, like CREATE TABLE or ALTER TABLE), DML (the ones that change the data itself, like UPDATE and DELETE), and ETL jobs (Extract, Transform, Load — the batch jobs that move data in and out of a database, which is another way of saying "the nightly job that syncs your warehouse").
The cluster endpoint always resolves to whichever instance is currently the primary. If your primary fails and Aurora promotes a replica to take its place, the cluster endpoint updates automatically to point at the new primary — you don't touch a connection string, you don't redeploy anything. That's the entire value proposition of using it instead of hardcoding an instance address.
The reader endpoint
The reader endpoint provides connection-balancing across whatever Aurora Replicas exist in the cluster. Aurora clusters support up to 15 Aurora Replicas, and by routing your SELECT-heavy traffic here, you take that load off the primary instance and let it scale roughly with how many replicas you're running.
One detail trips up almost everyone who reads a summary instead of the actual page: the reader endpoint balances connections, not individual queries. When your app opens a new connection to the reader endpoint, Aurora picks a replica for that connection at that moment — and every query on that connection goes to the same replica until the connection closes. If you want each individual query spread across different replicas, you have to open a new connection per query, which is rarely worth the overhead. Connection pools that hold long-lived connections to the reader endpoint will naturally skew — a pool of ten long-lived connections opened all at once might land unevenly across two replicas, and there's nothing wrong with your setup; that's just how connection-time balancing works.
Instance endpoints
Every single DB instance in the cluster — the primary and each replica — has its own dedicated endpoint that always points at that exact instance and nothing else. You can't create, delete, or rename these; Aurora manages them for you, and you can only see an instance endpoint on that instance's own detail page in the console.
In normal day-to-day operation you almost never point application traffic at an instance endpoint. Its real job is diagnosis: you're chasing a performance problem on one specific replica, and you need to connect to that replica specifically — not "a" replica, not "whichever one the reader endpoint hands you" — so you can run diagnostics against it directly.
Custom endpoints
If your cluster has instances of different sizes or configurations — say, two small replicas for regular app traffic and one large replica reserved for a nightly reporting job — the built-in reader endpoint can't tell them apart. It'll happily send a heavy analytics query to one of your small instances and slow down everyone else's page loads. A custom endpoint fixes this by letting you define your own named endpoint that only routes to a subset of instances you choose.
✅ Why this is the one to use for mixed-size clusters
If any of your instances run a materially different workload than the rest — bigger reporting queries, a different capacity class, an instance you're about to decommission — build a custom endpoint around it instead of fighting the default reader endpoint's round-robin behavior. It's the only endpoint type Aurora gives you where you decide exactly which instances are eligible.
Finding your actual endpoints (not guessing from memory)
A surprising number of endpoint mix-ups start because someone copied an endpoint URL from an old Slack message instead of looking it up fresh. Every endpoint name embeds context — you'll notice reader endpoints contain cluster-ro- in the hostname where the writer endpoint just has cluster- — but that's a convention, not something to eyeball under pressure. Look it up properly instead.
- Console: open your cluster's instance details page. The cluster endpoint, reader endpoint, and any custom endpoints are all listed there together. Each instance's own instance endpoint only shows up on that specific instance's detail page.
- AWS CLI: run
aws rds describe-db-clusters --query '*[].{Endpoint:Endpoint,ReaderEndpoint:ReaderEndpoint,CustomEndpoints:CustomEndpoints}'to list the writer endpoint, reader endpoint, and any custom endpoints for every cluster in your current region in one shot. - RDS API: call the
DescribeDBClusterEndpointsoperation if you're wiring this into your own tooling rather than typing commands by hand.
Whichever method you use, always append the port number after a colon when you connect — the endpoint name alone isn't a full connection string.
The failover trap: why the "right" endpoint still sends you to the wrong instance
Here's where most "reader vs writer" confusion actually turns out not to be an endpoint-choice problem at all — it's a DNS caching problem wearing an endpoint-choice costume. You already picked the right endpoint. It failed anyway. Here's why.
A DNS name is a piece of text like my-cluster.cluster-abc123.us-east-1.rds.amazonaws.com that has to be translated into an actual IP address before your computer can connect to anything — that translation is what "DNS" (Domain Name System) does, and every time it happens, the answer gets cached for a little while so you're not re-asking on every single connection. Aurora's own DNS records use a very short cache lifetime — five seconds — specifically so that after a failover, clients pick up the new writer's address almost immediately.
The problem is that almost nothing respects that five-second number. Operating systems cache DNS answers longer. Some network layers cache them longer still. And critically, some application runtimes — the Java Virtual Machine is the classic offender — cache resolved addresses internally and, in some configurations, only refresh that cache when the process restarts. So a failover happens, Aurora updates the cluster endpoint correctly within seconds, and your app keeps talking to the old primary's cached IP address anyway — which is now a read-only replica — because nobody told your runtime the address was stale.
⚠️ What this actually breaks
A DB instance's role can flip during failover — the box that was your writer becomes a read-only replica, at the exact same IP address your app already had cached. If your client keeps sending writes to that cached address, you get read-only errors that look identical to a wrong-endpoint mistake, even though your connection string was correct the entire time. And it's not a rare edge case: Aurora hard-fences the demoted instance during failover, so the moment topology changes, that stale cached connection stops working — the only question is how long your client keeps trying it before giving up.
Jake had exactly this happen during a planned failover last spring, right before a scheduled maintenance window. "I checked the connection string three times," he said. "It was pointed at the cluster endpoint the whole time. I thought I was losing my mind." He wasn't — his app's connection pool had simply cached an IP address longer than Aurora expected anyone to.
Ethan's take: "People assume 'use the right endpoint' is the whole fix, and it's necessary but it isn't sufficient. You also have to make sure whatever's sitting between your code and the network — the JVM, a connection pool, an OS-level resolver cache — isn't holding onto an address longer than Aurora's own five-second TTL. If it is, you're back to square one during every single failover, planned or not."
The direct fix, if you're not ready to add infrastructure: make sure your client's DNS cache lifetime is set below 30 seconds. For Java applications specifically, that usually means explicitly setting the JVM's network address cache TTL rather than trusting the default, since the default in some configurations effectively never expires until the process restarts.
Single-instance clusters: the exception that causes half these bug reports
Worth repeating on its own, because it's the reveal this whole post opened with: if your cluster only has a primary instance and no Aurora Replicas at all, the reader endpoint connects you to that primary instance instead. And because that's the only game in town, you can perform write operations through the reader endpoint in that specific state.
This isn't a bug — it's Aurora deliberately degrading gracefully rather than refusing every read connection just because you haven't provisioned a replica yet. But it means code that "correctly" uses the reader endpoint can pass every test you throw at it on a scaled-down dev cluster, then break the instant that same code runs against a production cluster with replicas attached — or, just as commonly, the reverse: code that's quietly writing through the reader endpoint in a single-instance dev environment sails through testing, then fails the moment a replica is added.
| Cluster state | Reader endpoint routes to | Can you write through it? |
|---|---|---|
| Primary + 1 or more replicas | One of the replicas | No — rejected |
| Primary only, no replicas | The primary itself | Yes — silently allowed |
The practical lesson: never validate your read/write split against a single-instance cluster and call it done. Test it against a topology that actually has at least one replica attached, or the test isn't exercising the behavior you care about.
Building a custom endpoint step by step
Custom endpoints come in two flavors that only really differ if you create them through the CLI or API. Through the console, every custom endpoint you make is type ANY, and you can't change that — the console simply doesn't expose the choice. An ANY endpoint can include both reader and writer instances, and Aurora directs new connections to any associated instance with equal probability. Through the CLI or the RDS API, you can additionally create a READER type, which is limited to reader instances only, but which automatically adjusts membership when a replica is promoted to writer — the newly promoted instance drops out of that endpoint on its own. An ANY endpoint doesn't do that automatic adjustment; if a member instance changes role, it stays a member regardless.
- Decide which instances belong together. Group by capacity or purpose — for example, your two small general-purpose replicas versus one large reporting replica.
- Create the endpoint with static membership, naming the exact instances:
aws rds create-db-cluster-endpoint --db-cluster-identifier my-aurora-cluster --db-cluster-endpoint-identifier static-endpoint --endpoint-type READER --static-members reader-1 reader-2 - Or use an exclusion list instead, if you'd rather include every instance automatically except the ones you name — useful with auto scaling, since new replicas get swept in without you touching the endpoint again:
aws rds create-db-cluster-endpoint --db-cluster-identifier my-aurora-cluster --db-cluster-endpoint-identifier app-reads-endpoint --endpoint-type READER --excluded-members analytics-reader-1 analytics-reader-2 - Confirm it exists with
aws rds describe-db-cluster-endpoints --region <your-region>, which lists both your built-in WRITER and READER endpoints alongside every custom endpoint and its type. - Point the right application, and only that application, at the new endpoint. Leave everything else on the built-in reader endpoint unless it genuinely needs the isolation.
One consideration that surprises people: a single DB instance can belong to more than one custom endpoint at once, and when a new instance joins the cluster — whether you added it manually or Aurora's own autoscaling did — it automatically joins every custom endpoint it's eligible for. That's convenient for exclusion-list endpoints and occasionally surprising for static-membership ones, since static membership doesn't auto-update the same way.
Where this bites hardest: Lambda, containers, and anything that scales in bursts
Everything above assumes a fairly steady number of connections — a handful of application servers, each holding a pool open to the writer and reader endpoints. Serverless compute breaks that assumption completely, and it's worth its own section because the failure mode looks different even though the root cause is the same endpoint confusion.
An AWS Lambda function runs inside something called an execution environment — think of it as a short-lived sandbox that Lambda spins up to run your code. Lambda can reuse that same environment across multiple invocations if requests come in close together, which is why the standard advice is to open your database connection outside your handler function, at the top level, so it survives between invocations instead of opening a fresh one every single time. But under a traffic spike, Lambda doesn't reuse — it scales out, creating brand-new execution environments in parallel, each one opening its own connection to whichever endpoint your code points at. A database with a fixed connection ceiling has no way to know that 300 of those new connections are really "the same application"; it just sees 300 separate clients arriving at once.
Containers on ECS or EKS run into a milder version of the same thing during a scale-out event or a rolling deployment: every new pod or task starts cold, opens its own pool, and if your reader endpoint is already carrying a healthy pool from the tasks that didn't restart, the new arrivals pile on top rather than replacing anything.
🕐 What changes with elastic compute
- Before: a fixed set of app servers, each with a long-lived pool — endpoint choice was the whole story.
- Now: bursty compute that can open far more simultaneous connections than a traditional fleet ever would, on both the reader and writer side.
- What that means: the correct endpoint still matters exactly as much, but connection-count management becomes an equally real problem sitting right next to it.
This is the scenario Amazon RDS Proxy was built for as much as failover was. Because it pools and shares database connections rather than letting every client hold its own, it lets a burst of Lambda invocations or container restarts share a much smaller number of real database connections underneath, instead of each one claiming a permanent seat. If your team is chasing a mysterious "too many connections" error that only shows up during traffic spikes, and you've already confirmed everyone's pointed at the correct endpoint, connection pooling — not endpoint choice — is almost certainly the actual problem.
RDS Proxy: the fix for the DNS trap, not a replacement for choosing endpoints correctly
Amazon RDS Proxy is a fully managed proxy layer you can put in front of your cluster. It matters here for one specific reason: it connects directly to your DB instances and tracks which one is the writer using Aurora's own cluster metadata, rather than relying on DNS resolution and caching at all. That sidesteps the entire stale-DNS problem from the section above, because there's no client-side address cache to go stale in the first place.
During a failover, RDS Proxy automatically connects to the newly promoted instance while preserving your application's existing connections to the proxy — Amazon states this can reduce failover-related downtime by up to 66% compared to waiting on DNS propagation. If you use it with a Multi-AZ DB cluster that has two readable standbys, typical failovers finish in well under 35 seconds. It also pools and shares connections between your app and the database, which separately helps with connection-exhaustion problems that have nothing to do with failover at all — the exact serverless scenario described above.
RDS Proxy gives you a default read/write endpoint, and for Aurora clusters you can add additional read/write and read-only proxy endpoints on top of it — so the reader-vs-writer distinction doesn't disappear once you add a proxy, it just moves one layer up. You still connect your read-only workloads to the proxy's read-only endpoint and your write traffic to its read/write endpoint; you're simply no longer at the mercy of DNS caching to get there.
🕐 What RDS Proxy does and doesn't replace
- It doesn't replace understanding the writer/reader distinction — you still route reads and writes to different proxy endpoints.
- It does remove DNS caching as a source of post-failover errors, since it tracks instance roles through cluster metadata instead.
- It adds connection pooling on top, which solves a separate, unrelated problem: too many short-lived connections overwhelming your database.
Cost-wise, RDS Proxy for provisioned Aurora instances is priced per vCPU-hour of the underlying database instance it's attached to; for Aurora Serverless, it's priced per Aurora Capacity Unit (ACU) consumed. There's no extra charge for the default proxy endpoint itself, but if you add supplementary read-only or read/write proxy endpoints, each one provisions its own AWS PrivateLink interface endpoint, which does carry its own separate charge. Partial hours bill in one-second increments with a 10-minute minimum after a billable change like creation or modification. By default, an AWS account is limited to 20 proxies, raisable via a Service Quotas request, and each proxy supports up to 200 associated Secrets Manager secrets.
🙋♂️ Jake's Reality Check
"So do I just always turn this on? It sounds like it fixes everything."
Not automatically, no. Watch for "pinning" — when RDS Proxy detects something in your session that isn't safe to hand off to another connection (a session variable you changed, a temp table, a prepared statement it can't be sure is portable), it locks that whole session to one specific database connection until it ends. Heavily pinned workloads lose most of the pooling benefit you were paying for. You can check this in CloudWatch: a high ratio of database connections to client connections, or a nonzero DatabaseConnectionsCurrentlySessionPinned metric, both point at pinning eating into your gains.
Splitting reads and writes in your application code
If you don't want to route everything through a proxy, the alternative is doing the read/write split in application logic — deciding per-query, in your own code, which connection pool to borrow from. Most ORMs and connection frameworks support "primary" and "replica" pool configuration for exactly this purpose; the primary pool points at your cluster endpoint, the replica pool points at your reader endpoint.
The catch with pure application-level splitting is that it inherits the DNS caching risk described earlier — your two pools are still resolving DNS themselves, on whatever schedule your runtime uses, so you still need to keep client-side TTLs short if you go this route without a proxy in front. Amazon also publishes an AWS JDBC driver wrapper specifically for Aurora that adds failover awareness on top of a standard JDBC connection — including a component called the Aurora Stale DNS Plugin, whose entire job is preventing your application from reconnecting to an old writer because of exactly the stale-DNS scenario covered above, plus a read/write splitting capability that lets your driver route statements to the correct instance type automatically based on cluster topology rather than a hardcoded endpoint choice.
| Approach | Solves stale DNS? | Extra infrastructure? | Best fit |
|---|---|---|---|
| Manual endpoint choice, short DNS TTL | Mitigates, doesn't eliminate | None | Small apps, low failover sensitivity |
| AWS JDBC wrapper driver | Yes, driver-level | None (drop-in driver) | JVM-based apps that can't add a proxy |
| RDS Proxy | Yes, bypasses DNS entirely | A managed proxy, billed per vCPU-hour | Serverless functions, connection-heavy apps, any language |
Ethan's blunt summary: "If your team is on Java or another JVM language and you can swap the driver, do that first — it's nearly free. If you're on Lambda, or you've got connection-exhaustion problems on top of the failover problem, RDS Proxy earns its cost. If you're small enough that a failover once a quarter causing thirty seconds of errors doesn't move the needle, fixing your DNS TTL might genuinely be enough."
Third-party poolers: PgBouncer and ProxySQL, and when they're not worth bothering with
RDS Proxy isn't the only connection pooler people put in front of Aurora, and if your team already runs one, it's worth understanding how it interacts with the reader/writer split rather than assuming it just works the same way.
PgBouncer is a lightweight PostgreSQL-only connection pooler, and it has three pooling modes that decide when a database connection gets handed back to the pool: session pooling holds the connection for the client's entire session (the most compatible, least aggressive option, and the default), transaction pooling returns the connection to the pool as soon as each transaction finishes, and statement pooling — the most aggressive — returns it after every single query, which disallows multi-statement transactions outright. That's a meaningfully different model from RDS Proxy's own multiplexing-with-pinning approach, and it's a decision you make yourself rather than one Aurora makes for you.
ProxySQL takes a different approach again, organizing backend instances into "hostgroups" — you assign your writer instance's own instance endpoint to a write hostgroup and your reader instances to a read hostgroup, and ProxySQL routes queries between them based on rules you define, rather than relying on Aurora's built-in cluster or reader endpoints at all.
⚠️ The honest trade-off with a third-party pooler
Because tools like PgBouncer and ProxySQL are usually pointed at instance endpoints directly rather than Aurora's own writer and reader endpoints, they don't automatically follow topology changes the way the built-in endpoints do. If a replica is promoted, or a new replica is added, or an instance is replaced, somebody has to update that tool's configuration to match — Aurora's own failover awareness doesn't extend into a third-party pooler unless you've specifically built automation to keep the two in sync.
None of that makes these tools a bad choice — plenty of teams run PgBouncer or ProxySQL successfully in front of Aurora for reasons that have nothing to do with failover, like fine-grained pooling control or multi-database routing rules RDS Proxy doesn't offer. But if the problem you're actually trying to solve is "my app keeps talking to the wrong instance after a failover," a third-party pooler adds a second layer that also needs to know about the failover, rather than removing the concern the way RDS Proxy's cluster-metadata approach does. Don't reach for one of these tools to solve a stale-DNS problem; reach for one when you need pooling behavior RDS Proxy specifically doesn't provide.
The security angle: don't bake a password into every endpoint choice
Once you've got reads and writes going to the right places, it's worth checking what's actually authenticating those connections, because the same endpoint-sprawl problem — multiple pools, multiple config files, multiple places a secret can go stale — tends to happen to credentials too.
Aurora supports authenticating to a DB cluster using AWS Identity and Access Management (IAM) database authentication, on both Aurora MySQL and Aurora PostgreSQL. Instead of a password, your application requests a short-lived authentication token — generated using AWS Signature Version 4, the same signing scheme used across AWS's APIs — and that token is only valid for 15 minutes. Nobody has to store or rotate a database password in application code at all; access is managed centrally through IAM instead, and for apps running on EC2, that can mean using the instance's own profile credentials rather than a password sitting in a config file. Consider IAM database authentication when your application creates fewer than 200 connections per second, since each authentication still involves a token-generation round trip.
✅ Why this pairs well with getting endpoints right
If you're already auditing every place in your codebase that opens a connection — to fix reader/writer mix-ups — that's the same moment to check whether each of those places is still holding a long-lived password instead of requesting a fresh IAM token. Fixing the endpoint but leaving a stale, over-permissioned password behind solves one problem while leaving the other one exactly where it was.
One limit worth stating plainly: IAM database authentication doesn't replace network-level security. Aurora clusters still need to run inside a VPC, with a VPC security group controlling exactly which devices and EC2 instances are even allowed to open a connection to the endpoint and port in the first place, and connections should still use TLS/SSL regardless of which authentication method you choose on top of that.
Proving which instance you're actually connected to
When something feels wrong with routing, the fastest way to cut through guesswork is confirming, from inside a live session, which instance you actually landed on and what mode it's in. For Aurora MySQL, checking the value of the @@innodb_read_only system variable tells you directly — a value of 0 means you're connected to the writer instance; anything else means you're on a replica. That single check settles arguments about "is it the endpoint or is it my code" faster than staring at connection strings.
Beyond that one-off check, keep describe-db-clusters or DescribeDBClusters in your back pocket for anything ongoing — they return your cluster's DB instances, whether each one is currently a reader, and its promotion tier (the order Aurora uses to decide which replica gets promoted first if the primary fails). If you're managing your own instance-endpoint connections directly rather than relying on the writer/reader endpoints, this is also how you programmatically discover the current, post-failover topology instead of assuming it hasn't changed.
Automating this so a human never has to eyeball an endpoint again
Every incident described in this post traces back to a human copying an endpoint value by hand at some point — from the console, from an old ticket, from memory. The lasting fix isn't "be more careful next time," it's removing the hand-copying step entirely.
At deploy time, have your infrastructure pipeline pull endpoint values fresh from describe-db-clusters or the equivalent RDS API call and inject them into environment variables or a secrets store, rather than letting a developer paste a value into a config file that then sits untouched for a year. If you provision your cluster with infrastructure-as-code, the writer, reader, and any custom endpoints are typically exposed as outputs from that same provisioning step — reference those outputs directly in the services that connect to the database instead of retyping them anywhere.
For catching drift after the fact, a scheduled check that runs describe-db-cluster-endpoints and diffs the result against what your application configuration currently expects will surface a stale custom endpoint, a renamed instance, or a static-membership endpoint that never picked up a newly added replica — all of which are silent failures until the day they aren't.
The practical checklist
Boil everything above down and here's what actually prevents the next incident:
- Route every code path that ever writes — even a rare one — to the cluster endpoint. No exceptions for "mostly read" jobs.
- Never validate your read/write split against a single-instance cluster; the reader endpoint's behavior there is fundamentally different.
- Set client-side DNS caching below 30 seconds anywhere you don't have a proxy layer absorbing that risk, and pay special attention to JVM-based apps, which often need this set explicitly.
- Reach for a custom endpoint the moment your instances stop being interchangeable in size or purpose — don't let the default reader endpoint silently send heavy queries to your smallest box.
- If you're on Lambda, ECS, or another elastically scaling compute layer, treat connection-count exhaustion as a separate problem from endpoint choice, not the same one.
- If failovers are costing you real downtime, evaluate RDS Proxy or the AWS JDBC wrapper driver before writing your own retry-and-reconnect logic from scratch.
- If you do adopt RDS Proxy, watch your pinning metrics — a proxy that's mostly pinned isn't giving you the pooling benefit you added it for.
- If you already run PgBouncer or ProxySQL, make sure something keeps their instance configuration in sync with Aurora's topology — they don't do that automatically the way the built-in endpoints do.
And to be direct about the limits of a checklist like this one: we can tell you what Aurora's endpoints do and about failover, pooling, and authentication — we can't tell you how your specific ORM, connection pool library, or in-house framework handles DNS caching internally, and that's genuinely something only your own framework's documentation and configuration can answer. If in doubt, verify your specific driver's TTL behavior directly rather than assuming it matches the general guidance above.
Frequently asked questions
What's the actual difference between the Aurora reader endpoint and the writer endpoint?
The writer (cluster) endpoint always points to the current primary instance and is the only one that accepts write operations. The reader endpoint connection-balances across Aurora Replicas and is meant for read-only queries, with one exception.
Can I write to the reader endpoint by accident?
Not if the cluster has at least one Aurora Replica, since Aurora rejects writes attempted through the reader endpoint in that case. But with only a primary and no replicas, the reader endpoint connects to the primary and writes succeed silently.
What happens to the reader endpoint if my cluster has no replicas?
It connects you to the primary instance instead of a replica, and behaves like a read/write endpoint since the primary is the only instance available.
Why did my write suddenly fail with a "read-only" error after nothing changed in my code?
Usually either a replica was added to a cluster that previously had none, removing the silent write-through-reader-endpoint behavior, or a failover happened and cached DNS still points at the old primary, now a read-only replica.
How long does it take for the cluster endpoint to update after a failover?
Aurora's DNS records use a five-second time-to-live and update almost immediately. Delays usually come from client-side or operating-system DNS caching holding a longer TTL than Aurora intends.
Does the reader endpoint load-balance individual queries or just connections?
Just connections. Aurora picks a replica when a connection opens, and every query on that connection goes to the same replica for its lifetime.
Can I use the reader endpoint for a long-running reporting job?
Yes if the job is fully read-only, but the whole job runs against one replica, which can affect other readers sharing it. A custom endpoint pointed at a dedicated instance avoids that.
What's an instance endpoint for, and when would I actually use one?
It connects to one specific DB instance permanently. Use it for diagnosing or tuning that instance, not for regular application traffic, since it doesn't follow failovers.
How is a custom endpoint different from the reader endpoint?
The built-in reader endpoint includes every Aurora Replica automatically. A custom endpoint lets you define your own subset of instances, useful when instances differ in size or purpose.
Do I need RDS Proxy if I already use the reader/writer endpoints correctly?
Using correct endpoints doesn't remove DNS caching risk during failovers on its own. RDS Proxy connects using cluster metadata instead of DNS, removing that risk and adding pooling, but it's added cost and infrastructure.
Will RDS Proxy add much cost to a small Aurora cluster?
RDS Proxy for provisioned instances is priced per vCPU-hour of the attached database instance, or per Aurora Capacity Unit for Aurora Serverless. The default endpoint has no extra charge; additional proxy endpoints add PrivateLink charges.
What is "pinning" in RDS Proxy and why does it matter?
Pinning happens when RDS Proxy can't safely reuse a connection for another client, usually due to session state changes. Once pinned, the connection is locked to one session until it ends, reducing pooling benefit.
How many Aurora Replicas can I actually add to a cluster?
Up to 15 Aurora Replicas per cluster, all automatically load-balanced by the built-in reader endpoint.
Is it safe to point my ORM's read replica config at the reader endpoint?
Generally yes, as long as that pool is never used for writes under any code path, since it may write through silently if the cluster temporarily has no replicas.
How do I find my cluster's endpoints without digging through the console?
Run the AWS CLI command describe-db-clusters with a query for Endpoint, ReaderEndpoint, and CustomEndpoints, or call the DescribeDBClusterEndpoints RDS API operation.
What's the single most common mistake teams make with Aurora endpoints?
Testing a read/write split against a single-instance dev cluster and assuming it proves anything, since the reader endpoint behaves differently once a replica is actually attached.
Revision note. Written September 2026, covering Amazon Aurora's cluster, reader, instance, and custom endpoints, Amazon RDS Proxy, IAM database authentication, and how PgBouncer and ProxySQL fit alongside Aurora's own endpoints, at the time of writing. AWS updates RDS Proxy pricing and quota details periodically, so double-check current figures on the RDS Proxy pricing page before budgeting. If you've spent a weekend chasing a "read-only" error that made no sense, you're not missing something obvious — this is a genuinely easy trap to fall into, and now you know exactly where to look.