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.

Programmer DD
Programmer DD
Programmer DD
How a Spring Boot Upgrade Cut Our AWS Bill by 45%

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 → Database

In 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=60000

The 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=30

This 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=30

The 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=true

This 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.

spring-bootdatabase-connectionaws-cost-optimizationperformance-tuning
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.