How a Spring Boot Upgrade Cut Our AWS Bill by 45%
Facing a soaring AWS bill despite stable traffic, we traced the cost to over‑provisioned EC2 instances and leaking database connections, then upgraded to Spring Boot 3.5 and applied targeted pool, transaction, and JPA tweaks, achieving a 45% reduction in monthly spend while boosting performance and reliability.
Cloud Cost Crisis
In the last quarter the AWS bill rose to $27,000 even though the number of customers stayed around 5,000 and daily orders were about 120,000. The CFO demanded cost cuts or price hikes, but raising prices was not feasible.
Identifying the Biggest Cost Drivers
Using AWS Cost Explorer with detailed tags we discovered the spend breakdown:
EC2 instances : 58%
RDS PostgreSQL : 25%
Data transfer : 12%
Other services (Redis, S3, etc.) : 5%
EC2 costs were the primary target. Further analysis showed that the number of running instances far exceeded actual traffic, causing auto‑scaling to fire and launch under‑utilised instances.
Resource Utilisation Mystery
Metrics revealed a pattern where each EC2 instance started healthy but after about 12 hours showed rising CPU (70‑80%), growing JVM heap, slower response times and reduced throughput, eventually triggering another scaling event.
Database Connection Leak
Detailed performance monitoring exposed that the application opened too many database connections, many of which were never closed. The typical request flow should be:
Request → Controller → Service → Repository → DatabaseIn reality each request opened five connections, closed three and leaked two.
The leak stemmed from custom Spring Data JPA repositories that did not manage transaction boundaries correctly.
Spring Boot 3.5 Insights
Spring Boot 3.5 introduced several improvements:
Stronger connection‑pool integration
Improved transaction management
Smarter resource cleanup
Better handling of lazy‑loading scenarios
We decided to upgrade and apply key configuration changes.
Key Configuration Changes
1. Connection‑Pool Optimisation
We adjusted HikariCP settings:
# before
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.minimum-idle=10
# after
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=120000
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.leak-detection-threshold=60000The new leak-detection-threshold helped identify leaking connections, and lowering minimum-idle avoided idle connections during low traffic.
2. Transaction Management Improvements
We enabled delayed acquisition and release after transaction:
# before
spring.jpa.properties.hibernate.connection.provider_disables_autocommit=true
# after
spring.jpa.properties.hibernate.connection.provider_disables_autocommit=true
spring.jpa.properties.hibernate.connection.handling_mode=DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION
spring.transaction.default-timeout=30This setting ensures connections are only obtained when needed and released immediately after the transaction.
3. JPA Query Optimisation
Batch processing and query optimiser were enabled:
# batch processing
spring.jpa.properties.hibernate.jdbc.batch_size=50
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=true
# query optimiser (new in 3.5)
spring.jpa.properties.hibernate.query.optimizer.enabled=true
spring.jpa.properties.hibernate.default_batch_fetch_size=30The optimiser reduces N+1 queries and chooses optimal join strategies.
4. Statement Caching
Prepared‑statement cache settings were added:
spring.datasource.hikari.data-source-properties.cachePrepStmts=true
spring.datasource.hikari.data-source-properties.prepStmtCacheSize=250
spring.datasource.hikari.data-source-properties.prepStmtCacheSqlLimit=2048
spring.datasource.hikari.data-source-properties.useServerPrepStmts=trueThis cuts CPU usage on the database.
5. Scenario‑Specific Connection Management
We added explicit transaction boundaries and async processing for heavy‑weight operations:
@Service
public class InventorySyncService {
@Transactional(timeout = 60)
@QueryHints(@QueryHint(name = "org.hibernate.fetchSize", value = "100"))
public void synchronizeInventory() {
// resource‑intensive logic
}
}
@Async("taskExecutor")
public CompletableFuture<Void> processInventoryUpdates(List<InventoryUpdate> updates) {
return CompletableFuture.completedFuture(null);
}Implementation and Immediate Impact
After thorough testing in a pre‑production environment, we deployed the changes. Results included:
Average DB connections per instance dropped from 7.8 to 3.2
Connection acquisition time reduced by 68%
No connection leaks detected during a 72‑hour stress test
Production Rollout and AWS Cost Impact
Post‑deployment metrics showed:
EC2 CPU utilisation fell from 62% to 28%
JVM GC pause time decreased by 76%
Throughput increased by 120% and average response time fell from 187 ms to 74 ms
Required EC2 instances during peak dropped from 20‑24 to 9‑10
Before rollout: $27,000 per month
After rollout: $14,850 per month
Total saving: $12,150 (45%)RDS costs fell 32% and data‑transfer costs 15% due to more efficient queries.
Technical Deep Dive: Why It Works
The delayed acquisition mode prevents early connection grabs, while the new optimiser eliminates N+1 queries and adjusts fetch sizes automatically. Combined with a properly sized HikariCP pool and statement caching, the application can handle the same load with far fewer resources.
Best Practices
Size the connection pool based on (max DB connections – reserved) / number of app instances.
Enable detailed connection metrics and leak detection.
Maintain environment‑specific profiles (smaller pools for dev, larger for prod).
Regularly review query performance and enable Hibernate statistics.
Unexpected Benefits
Beyond cost savings we observed:
Improved developer experience with clearer error messages.
More accurate load‑testing results.
Significant reduction in operational alerts and auto‑scaling events.
Environmental impact: server count reduced from 24 to 10, cutting energy consumption.
Conclusion
Optimising configuration—especially database connection handling—can deliver massive cost reductions and performance gains without major code rewrites. Spring Boot 3.5 provides the necessary hooks; the real value comes from careful analysis, targeted tuning, and continuous monitoring.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
