A production PostgreSQL database had 200 GiB of allocated Amazon RDS storage but was using only around 20 GiB.
The database had grown into this configuration over time, but the allocated capacity was now far beyond its real needs. Keeping it was not causing an immediate technical failure, but it meant paying for unused storage and carrying an unnecessarily oversized configuration indefinitely.
The goal looked simple:
Reduce the allocated storage from 200 GiB to 40 GiB without putting production data at unnecessary risk.
The implementation was less simple. This was a live database used by several services, so rebuilding it casually or accepting a long maintenance window was not reasonable.
The constraint
The existing RDS instance could not simply be edited from 200 GiB down to 40 GiB through the normal storage modification flow.
Increasing storage is straightforward. Reducing it requires creating a replacement environment with the desired storage configuration and then moving production to it.
RDS Blue/Green provided that migration path: it created a separately configurable replacement database, kept it synchronized with production, and provided a managed switchover once the new environment was ready.
The main options were:
- Export and restore the database into a new instance.
- Use logical replication between two independently created databases.
- Create an RDS Blue/Green deployment and perform a controlled switchover.
A manual export and restore would have been easy to understand, but it would also have introduced a longer period during which writes needed to be stopped or synchronized separately.
Building our own logical replication process would have provided more control, but also more moving parts to configure, test and later remove.
For this database, the Blue/Green approach offered the best balance between control and operational simplicity.
The migration shape
The process looked approximately like this:
Existing production database
PostgreSQL, 200 GiB
|
| continuous replication
v
Green environment
PostgreSQL, 40 GiB
Autoscaling enabled up to 150 GiB
|
| validation and controlled switchover
v
New production database
PostgreSQL, 40 GiB
The original database remained the production system while the green environment was created and synchronized.
That separation was important. It allowed us to prepare and inspect the new environment without changing the database currently serving the application.
Choosing the target size
The database was using approximately 20 GiB, but I did not set the replacement storage to exactly that amount.
A database needs room for:
- future data growth;
- temporary operations;
- indexes;
- transaction logs;
- maintenance activity;
- unexpected increases in usage.
We selected 40 GiB as the new initial allocation. This removed most of the unused capacity while leaving reasonable headroom.
Storage autoscaling was also enabled with a maximum of 150 GiB.
This meant the database could grow automatically if needed, but it would not immediately return to the previous oversized allocation.
The objective was not to find the smallest possible number. It was to create a more sensible starting point with a controlled growth path.
Creating the green environment
I created an RDS Blue/Green deployment from the production database and configured the green database with the new storage settings.
During this stage, the original database continued receiving production traffic. The green environment remained isolated from application writes and received changes through replication.
Creating the environment was only the beginning. A replacement database being marked as available does not mean it is ready to become production.
The important part was validating its state.
Watching replication
Replication lag was not immediately zero. At one point it was approximately 58 seconds.
That was not itself a failure. It meant that the green environment was still catching up with production.
The important questions were:
- Was replication still progressing?
- Was the lag stable, increasing or decreasing?
- Were there any replication errors?
- Was the green database receiving recent changes?
- Would switching at this point lose or delay expected data?
We waited rather than treating the existence of the environment as proof that it was ready.
This is a small operational detail, but it is also where many risky migrations go wrong. Infrastructure creation is visible and satisfying. Waiting for synchronization and checking boring details feels less productive, but it is usually the more important work.
Validating the new database
Before switchover, I checked the new environment from several directions.
The checks included:
- confirming the expected PostgreSQL version and configuration;
- checking the allocated storage and autoscaling limit;
- verifying that replication was healthy;
- comparing row counts in important tables;
- checking known production records;
- confirming that recent changes appeared in the green database;
- checking application connectivity requirements;
- reviewing monitoring and security configuration.
One of the largest tables was a receipt-items datamart containing a very large number of rows. Comparing every row individually would have been slow and difficult to interpret, while one hash for the entire table would have hidden where a mismatch occurred.
Instead, the data was validated in stable partitions, such as by business date and restaurant. For each partition, I compared the row count, the latest source timestamp and a deterministic checksum generated from selected business columns in a stable order.
Conceptually, the result looked like this:
Partition Blue rows Green rows Blue checksum Green checksum Result
--------------------------------------------------------------------------------------
2026-06-01 / 013 184,291 184,291 8f2a... 8f2a... OK
2026-06-02 / 013 176,804 176,804 31bd... 31bd... OK
2026-06-03 / 013 191,442 191,442 b782... b782... OK
Partitioning made failures easier to locate and avoided treating one enormous checksum as sufficient evidence. The checksum also needed deterministic serialization and ordering; otherwise two equivalent datasets could produce different results simply because rows were read in a different order.
These checks did not prove that every possible application path was correct, but they gave strong evidence that the large analytical dataset had been copied completely and consistently.
The validation list was written down before the switchover. That reduced the chance of forgetting a check while concentrating on the live migration.
Switchover
Once replication was sufficiently caught up and the validation checks passed, I initiated the Blue/Green switchover.
We treated this as a real production change, not as a harmless infrastructure action.
The application and database were monitored during and after the switch. The immediate checks included:
- database availability;
- application health;
- authentication and user access;
- recent writes;
- background processing;
- error logs;
- database connections;
- storage configuration.
After the switchover, the production database retained its expected identity from the application’s perspective, but it was now backed by the new 40 GiB environment.
The previous production instance became the old blue environment.
Cleanup without removing the safety net too early
A successful switchover did not mean the migration was finished.
The new production environment was observed and validated again before the old instance was removed. The old database was then deleted, but a final snapshot was retained.
This provided a recovery option without continuing to pay for a complete unused database instance.
The remaining Blue/Green deployment metadata was also cleaned up after confirming that it was no longer needed.
The final state was:
Production storage: 40 GiB
Storage autoscaling: enabled
Maximum storage: 150 GiB
Old database instance: removed
Final snapshot: retained
The process stayed safe for three main reasons:
- production remained authoritative until the controlled switchover;
- readiness was checked using both replication health and application-level data validation;
- the old environment was retained until the new production state had been confirmed.
What I would do differently
The migration succeeded, but I would automate more of the validation next time.
A reusable script could run the same checks against both environments, calculate partitioned checksums for the largest tables, and save a concise comparison report as part of the change record.
For example:
Check Blue Green Result
-----------------------------------------------------------------
Active users 53 53 OK
Latest receipt timestamp 19:42:11 19:42:11 OK
Receipt partitions checked 30 30 OK
Mismatched partition checksums 0 0 OK
Orphan reference check 0 0 OK
This would make the decision to switch more repeatable and reduce manual error.
I would also prepare a written rollback decision before starting. Not only “we can roll back,” but:
- which symptoms require rollback;
- who makes the decision;
- how long we observe before deciding;
- which data written after switchover must be considered.
Finally, I would add a periodic check comparing allocated storage with actual usage. The original problem developed gradually. A simple capacity review could have identified it earlier.
The main lesson
The interesting part of this change was not reducing a number from 200 to 40.
It was replacing a production database while preserving confidence in the result.
Managed cloud services remove a large amount of operational work, but they do not remove the need for an operational process. The migration still required understanding the platform constraint, observing synchronization, validating business data, planning the switchover and retaining a recovery path.
The final database was smaller and less expensive, but the more important result was that the change was controlled and explainable.