This is not a serious proposal for Doctolib's stack. I take the problem the company has described in public, make assumptions around it, and walk towards an answer with cost, latency and money in view. Everything runs on AWS, by choice. Doctolib's real platform is its own, and I make no claim about it or about its bill.
Every amount below prices my exercise. Prices are AWS list prices in USD before tax, for the EU (Paris) Region, from the offer files in the sources. List prices overstate what a company of this size pays, and private discounts are not public. Each figure is tagged public, inferred or assumed, and each comes from one script that applies every lever only to what the previous lever left.
The problem
Doctolib has talked in public for five years about one problem: how far one PostgreSQL database goes. In 2022, talks described one writer and 13 replicas and up to 20 million queries a minute. The team said it was close to the physical limits of Aurora, and that one writer was no longer enough. In October 2025, a podcast gave the state of things:
We have 10 writers today and each writer can have up to 15 readers
The same episode says the main writer holds about 40 terabytes, which is "not really the issue": "That's more the IO the problem." Readers scale by adding instances. A writer does not. So the wall is writes, and I take that as my starting point. How the company solved it is not my subject here. How far money and latency can go from a careless start is.
What I assume
One public ratio anchors the model. The 2021 retrospective names db.r5.24xlarge, at 96 vCPUs. Fourteen of those at 20 million queries a minute is 248 queries a second per vCPU. It is an observed load in a real fleet, headroom included, and the class in 2022 is my inference.
The scale behind the exercise
| Quantity | Low | Base | High | Tag |
|---|---|---|---|---|
| Peak queries a second | 500,000 | 667,000 | 1,000,000 | Assumed: ×1.5, ×2, ×3 on the public 2022 figure |
| Queries per vCPU at peak | 248 | 248 | 248 | Inferred |
| Writes | 7% | 7% | 7% | Assumed |
| Average over peak | 40% | 40% | 40% | Assumed |
| Billed I/O per query | 0.1 | 0.3 | 0.6 | Assumed |
| Data | 60 TB | 90 TB | 130 TB | Assumed: about 40 TB on the main writer is public |
| Traffic through NAT, a month | 20 TB | 50 TB | 100 TB | Assumed |
I chose 7% because it puts a writer at its limit in 2022, when the team said one writer was no longer enough. On the same ratio, one 192-vCPU writer carries about 47,600 queries a second (assumed as a limit).
The fleet here is sized from that ratio, so it is smaller than what 10 writers with up to 15 readers each would suggest. It says what a load of this size needs in my model, not what runs anywhere. Aurora, ElastiCache, DynamoDB, S3 and NAT gateways all have Paris price rows, so nothing here leaves the Region. I price one Region only.
Version 0: the naive build
The first version is the obvious one: one Aurora cluster, one writer and as many readers as the load asks, on the largest Intel class, at $32.16 an hour. No cache, no pooler, Aurora Standard storage, S3 traffic through NAT gateways, and no commitment.
Requestsfrom the app
Routingreads and writes
One writerevery write
14 readersevery read
NAT gatewaysS3 traffic
Answersone stream back
Version 0, USD a month at list, rounded
| Line | Low | Base | High |
|---|---|---|---|
| Readers | 234,800 (10) | 328,700 (14) | 469,500 (20) |
| Writer | 23,500 | 23,500 | 23,500 |
| Billed I/O ($0.22 per million) | 11,600 | 46,300 | 138,800 |
| Storage ($0.11 per GB-month) | 6,600 | 9,900 | 14,300 |
| Cross-zone traffic ($0.01 per GB) | 7,000 | 9,300 | 14,000 |
| NAT gateways | 1,100 | 2,600 | 5,100 |
| Backup beyond the free allocation | 300 | 1,000 | 2,900 |
| Total | 284,900 | 421,200 | 668,100 |
| Writer at peak | 73.5% | 98% | 147% |
Instances are 84% of the base. The base writer sits at 98% at peak, which is the wall. The high column does not fit at all: 20 readers is over Aurora's limit of 15, and its writer is past its limit. Version 0 is cheap on paper because it cannot carry the load, so the page has to spend money somewhere.
Optimisation 1: a read cache, and its hit rate
The problem. Fourteen readers answer the same availability questions again and again.
The change. The team added a cache for availability after its 2021 peak, and I start there. Six cache.r8g.4xlarge Valkey nodes, at about $7,225 a month. I assume 30% of reads are cacheable, at an 80% hit rate, so 24% of reads leave the readers.
Latency. A hit is about 1 ms against about 5 ms on a reader (assumed). The real cost is staleness: an old slot is a failed booking.
Dollars. Readers fall from 14 to 10, and it saves about $99,100. That leaves about $322,200. At a 60% hit rate it saves about $72,500. At 90%, about $100,600, since no further whole reader goes. The trap is the cold cache. Those 10 readers carry 1,920 vCPU, and with the cache empty at peak the reads ask for 2,500, or 130%. A restart at the wrong hour takes the fleet down, so the cache needs warming, and it now counts as capacity.
- Added
- Replaced
Optimisation 2: the number of writers, the lever that adds money
The problem. Nothing above touches writes. The base writer is still at 98% at peak.
The change. Split by domain, moving "a group of tables that are very related to each other", to keep joins and foreign keys. I treat 60% at peak as safe, and price three counts.
How many writers, from the state after the cache
| Writers | Largest writer at peak | At twice the load | Adds a month |
|---|---|---|---|
| 2 | 58.8% | 117.6% | 23,477 |
| 4 | 53.9% | 107.8% | 46,954 |
| 10 | 39.2% | 78.4% | 70,430 |
Latency. A query inside a domain is unchanged. A join across domains becomes two queries in the application (assumed: a few milliseconds, and a project for each one).
Dollars. I take two, the cheapest count under 60%. It adds about $23,500, which leaves about $345,600. This is the one lever here that adds real money, and it is the reason the others are possible. Two clear today's wall in my model and fail at twice the load, so two is only the cheapest answer to this one row. Ten is the count in this table that survives a doubling, which is what a growing company has to plan for, and it costs about $47,000 a month more. That figure comes from my fleet, not a real one, and it leaves out isolation and blast radius, which this table cannot price.
- Added
- Replaced
Optimisation 3: connection pooling
The problem. A large Rails monolith holds many idle connections, and each one costs memory on the database.
The change. Two options, and the first is a trap. RDS Proxy bills $0.022 per vCPU-hour on the fleet behind it: about $37,000 a month on 2,304 vCPU. It also pins connections on SET, on prepared statements and on nextval, which erodes what it was bought for. So I run a pooler on six small Graviton nodes, at about $417. Transaction pooling has its own price: session state and prepared statements have to change in the application. I assume 8% of database capacity comes back.
Latency. One more hop, under a millisecond (assumed). The pages I read state no figure for RDS Proxy.
Dollars. In the three columns it frees no whole reader yet, so it costs $417, which leaves about $346,100. I keep it for the 8% of headroom, and it is the only lever I keep at a loss.
- Added
- Replaced
Optimisation 4: moving a domain out
The problem. Some domains have no need for PostgreSQL's joins and pay for its writer anyway.
The change. An append-heavy domain moves to DynamoDB, taking 10% of writes, 8% of reads and 5 TB. At Paris on-demand rates that costs about $11,020 a month. Foreign keys and transactions across that boundary go, and the migration is a real project.
Latency. AWS states single-digit milliseconds for DynamoDB, and I assume about the same as a reader. The gain is on the writer: the largest falls from 54.1% to 48.7% at peak.
Dollars. It depends on rounding, which is the honest result. In the low and high columns it frees a reader and saves about $16,600 and $18,100. In the base it frees none, and costs about $6,900 a month more, so I leave it out. $346,100 stands. Move a domain to buy write headroom, or because the model suits it. As a saving, it needs the volume to free a whole instance.
Optimisation 5: readers follow the day
The problem. The fleet is sized for a peak, and the average is 40% of it.
The change. Aurora Auto Scaling adds and removes replicas. I assume 30% of the reader hours above one reader per cluster go at night and at weekends.
Latency. A new reader starts with a cold buffer cache, so its first minutes are slow, and a peak comes fast (assumed). Scale out ahead of the morning, on a schedule.
Dollars. About $56,300, which leaves about $289,700. Only the cache and the commitment save more, and a schedule is the cheapest of the three to undo.
- Replaced
Optimisation 6: Graviton classes
The problem. Version 0 runs on Intel, because that is where I started it.
The change. Move managed services first. db.r8g against db.r7i is $30.53 against $32.16 at the largest size, 5.1% less at list. Test on one reader, then fail over.
Latency. I count nothing. A throughput gain is likely but unmeasured here, and if one appears it frees readers on top of this.
Dollars. About $11,400, which leaves about $278,300.
- Replaced
Optimisation 7: reads stay in their zone
The problem. Traffic between application and database across zones bills $0.01 per GB. With 2 KB a query and two thirds of traffic crossing, that adds up.
The change. Route each read to a reader in the same zone, with zone-aware routing in the application, since the reader endpoint does not do it. I assume 70% of cross-zone read traffic goes. Writes still cross, and a failed zone brings it all back.
Latency. A slightly shorter hop (assumed: under a millisecond).
Dollars. About $4,600, which leaves about $273,600.
- Replaced
Optimisation 8: cold rows to Amazon S3
The problem. Old rows sit on storage that exists for live queries.
The change. Move 30 TB to Amazon S3 as columnar files, compressed three to one (assumed), in Glacier Instant Retrieval at $0.005 per GB. The files are large, so the 128 KB rule does not bite, and the 90-day minimum suits data that never changes.
Latency. A cold row goes from milliseconds to a query engine's seconds (assumed).
Dollars. About $3,600, which leaves about $270,100. On 90 TB, storage was never the problem, or as the podcast puts it: "Not because of the amount of data." Do this for query speed if it helps. As a cost lever, at this scale, it is noise.
- Added
- Replaced
Optimisation 9: S3 traffic off the NAT gateways
The problem. 50 TB a month pays $0.05 per GB to reach a service inside the Region.
The change. A gateway endpoint for S3 has no charge. I assume 80% of that traffic is S3.
Latency. None worth counting.
Dollars. $2,000, which leaves about $268,100. It takes an afternoon, and that is why it is on this page.
- Added
- Replaced
Optimisation 10: I/O-Optimized against Standard
The problem. Doctolib said "That's more the IO the problem", so this is the lever people reach for.
The change, and why I do not make it. AWS calls it "the best choice when your I/O spending is 25% or more of your total Aurora database spending". Here I/O is 14%. Switching adds 29.87% to the instance rate, or about $63,900, and $8,280 of storage, to remove $35,929 of I/O. It costs about $36,300 a month more.
Latency. None stated in what I read.
Dollars. $0. $268,100 stands. Break-even sits at about 124,900 billed I/O a second, as a monthly average, against my 62,100. That is the first number I would look up from the inside, because "IO" in a podcast may mean something else. Reserved Instances still apply: AWS says I/O-Optimized consumes 30% more normalized units. In the base the switch still costs about $16,700 a month after commitments. In the high column it saves $3,306 before commitments and about $27,300 after them, so there I take it.
Optimisation 11: Reserved Instances
The problem. Nine of the twelve instances run every hour of the year.
The change. Commit to the steady baseline, last. A reservation is tied to its class type, so buying it on Version 0's Intel fleet of 15 would have paid for instances that are now 12, on another class. One year, No Upfront, 33% off, on the two writers and seven readers.
The commitment on the same nine instances, USD a month
| Option | Discount | Saving |
|---|---|---|
| Database Savings Plan, 1 year | 20% | 40,114 |
| Reserved Instances, 1 year No Upfront | 33% | 66,188 |
| Reserved Instances, 3 years No Upfront | 53% | 106,302 |
Latency. None.
Dollars. About $66,200, which leaves about $201,900. The plan is the flexible choice and three years the deep one, but three years is a long bet on a class while the fleet is still splitting. The Paris offer lists that three-year No Upfront rate, while AWS's guide says No Upfront is a one-year option, so I would confirm it before planning on it.
Optimisation 12: negotiation
The problem. A company at this scale does not pay list.
The change. A private discount, and I assume 8%. Nothing about any real agreement is public, and it would apply to much more than this data tier.
Latency. None.
Dollars. About $16,200, which leaves about $185,700.
Where it lands
Requestsfrom the app
Poolerfewer connections
Valkey cachecacheable reads
10 readersGraviton, zone-aware
Two writerssplit by domain
Cold rowsAmazon S3, read rarely
Cache fillon a miss
Answersone stream back
Start and end, USD a month, rounded
| Low | Base | High | |
|---|---|---|---|
| Version 0 | 284,900 | 421,200 | 668,100 |
| After every lever | 133,600 | 185,700 | 283,300 |
| Cut | 53.1% | 55.9% | 57.6% |
| Instances, from and to | 11 to 9 | 15 to 12 | 21 to 16 |
The base ends on two writers and 10 readers, with the largest writer at 54% at peak. The order did the work. Four levers carry it: the cache, the schedule, the commitment and the discount. Two of the twelve stay out of the base, and one costs $417 on purpose. Unlike a token bill, this one never collapses in one step. It is won instance by instance, and rounding decides more than I expected.
The funnel
1 · $421,200Version 0
2 · $346,100Cache, writers, pooling
3 · $346,100One domain out
4 · $289,700Readers follow the day
5 · $278,300Graviton classes
6 · $268,100Zone, cold rows and NAT
7 · $268,100I/O-Optimized
8 · $201,900Reserved Instances
9 · $185,700Negotiated discount
- Version 0Base · One writer, 14 readers, largest Intel class$421,200 a month
- Cache, writers, poolingOptimisation · Cache saves $99,100; the rest adds $23,900$346,100 a month leftsaves $75,10017.8% of the base
- One domain outOptimisation · Does not pay here: $6,900 a month more$346,100 a month leftsaves $00% of the base
- Readers follow the dayOptimisation · 30% of the reader hours above the floor$289,700 a month leftsaves $56,40013.4% of the base
- Graviton classesOptimisation · 5.1% at list, no throughput gain counted$278,300 a month leftsaves $11,4002.7% of the base
- Zone, cold rows and NATOptimisation · Three small levers, about $10,200$268,100 a month leftsaves $10,2002.4% of the base
- I/O-OptimizedOptimisation · Does not pay here: $36,300 a month more$268,100 a month leftsaves $00% of the base
- Reserved InstancesCommitment · 1 year, 33% off, on 9 of 12 instances$201,900 a month leftsaves $66,20015.7% of the base
- Negotiated discountNegotiation · 8% assumed$185,700 a month leftsaves $16,2003.8% of the base
Your own numbers will differ, and yours can be measured. The calculator prices your stack line by line, and a FinOps audit starts from your real billing data, in the same order: reduce first, then commit, then negotiate.
What I cannot see from outside
- The real load and its mix. The last public query figure dates from 2022. My 7% of writes is a calibration, not a measurement.
- What a vCPU really carries. The 248 comes from one public moment, on an older class I inferred.
- The real I/O rate. It decides lever 10. "IO" in a podcast may mean query load, not billed storage I/O.
- The storage configuration. If a fleet already runs on I/O-Optimized, lever 10 is already taken and its instance rates are about 30% higher.
- Cacheable share and hit rate. I assumed both, and the largest optimisation rests on them.
- Why ten writers. The evaluation of 2022 is public, its conclusion is not. Isolation and blast radius may matter more than this page's table.
- Everything outside this data tier. The Kubernetes fleet, Kafka, Couchbase, CI and the feature-team databases are not here. Neither is a second Region.