Production engineering guide for reserved instance management covering patterns, implementation strategies, and operational best practices.
Reserved Instances (RIs) are a core mechanism for reducing cloud infrastructure costs by committing to a fixed capacity for a defined duration—typically 1 or 3 years—against a predictable workload. They are most effective when a workload runs consistently over time, with minimal churn in instance types, regions, and availability zones. This management process is essential in environments where instance utilization exceeds 70% over time and where predictable workloads—like backend APIs, data processing pipelines, and long-running databases—can benefit from upfront commitment. The primary goal is to minimize total cost of ownership by reducing per-hour instance costs through prepayment, while avoiding over-provisioning and under-utilization.
To begin, identify workloads that exhibit stable usage patterns across multiple months. Use historical metrics from the cloud provider’s monitoring service—cloudwatch, prometheus, or datadog—to extract average and 95th-percentile CPU and memory utilization over a 90-day window. The target is to find workloads where utilization remains above 60% for at least 80% of the time.
Once selected, determine the required instance type and configuration. For example, an application running on m5.xlarge across three availability zones in us-west-2 with an average CPU utilization of 72% and memory usage of 84% qualifies for RI commitment. Use the describe-instances API call to extract current instance metadata:
aws ec2 describe-instances \
--filters "Name=tag:Environment,Values=production" \
--query "Reservations[*].Instances[*].[InstanceId,InstanceType,Placement.AvailabilityZone,State.Name]" \
--output table
The output includes instance IDs, types, and availability zones. For each workload, calculate the total instance-hours per month:
# Example: Compute monthly instance-hours for a group of instances
aws ec2 describe-instances \
--instance-ids i-0123456789abcdef0 i-0987654321fedcba0 \
--query "Reservations[*].Instances[*].[InstanceId,InstanceType,Placement.AvailabilityZone,LaunchTime]" \
--output json
This yields a JSON blob with LaunchTime, InstanceId, InstanceType, and AvailabilityZone. Use this to compute total hours since launch, and project monthly usage.
For sizing, decide between standard and convertible RIs. Standard RIs apply to a specific instance type, while convertible RIs allow flexibility across instance families and sizes. The choice hinges on workload stability: use standard RIs for stable, predictable workloads; use convertible RIs for workloads that may evolve or scale.
To buy RIs, use the purchase-reserved-instances-offering API call. The key inputs are the offering ID, instance count, and instance type. Offering IDs are retrieved via describe-reserved-instances-offerings:
aws ec2 describe-reserved-instances-offerings \
--filters "Name=instance-type,Values=m5.xlarge" \
--filters "Name=offering-type,Values=Standard" \
--filters "Name=duration,Values=3650" \
--filters "Name=region,Values=us-west-2" \
--query "ReservedInstancesOfferings[*].[OfferingId,InstanceType,Duration,PricePerHour,FixedPrice,InstanceCount]" \
--output table
This returns a list of offerings with OfferingId, InstanceType, Duration (in hours), PricePerHour, FixedPrice, and InstanceCount. The FixedPrice is the total up-front cost; PricePerHour is the hourly rate after the up-front payment.
To purchase, send a purchase-reserved-instances-offering request with the selected OfferingId, desired InstanceCount, and optional InstanceIds to apply the RI to specific instances. The request is sent as a JSON body to the EC2 API endpoint:
{
"ReservedInstancesOfferingId": "r-0a1b2c3d4e5f67890",
"InstanceCount": 10,
"DryRun": false,
"InstanceIds": [
"i-0123456789abcdef0",
"i-0987654321fedcba0"
]
}
The response includes ReservationId, StartTime, Duration, FixedPrice, and InstanceCount. The ReservationId is used to track the purchase and to associate the RI with specific instances.
After purchase, apply RIs to running instances using the associate-reserved-instances API. The operation is idempotent: calling it multiple times with the same ReservationId and InstanceIds updates the association without duplication.
aws ec2 associate-reserved-instances \
--reservation-id r-0a1b2c3d4e5f67890 \
--instance-ids i-0123456789abcdef0 i-0987654321fedcba0
Each instance in the list receives the benefits of the RI: the hourly cost is reduced to the PricePerHour specified in the offering, and the up-front FixedPrice is charged at the start of the reservation.
If the instance is already running, the RI applies from StartTime onward. If StartTime is in the future, the instance is reserved but not charged until the start time. This allows for staggered rollout of RIs across a fleet.
After deployment, monitor RI utilization via describe-reserved-instances and describe-instance-status.
aws ec2 describe-reserved-instances \
--filters "Name=reservation-id,Values=r-0a1b2c3d4e5f67890" \
--query "ReservedInstances[*].[ReservationId,InstanceType,Duration,State,StartTime,EndTime,FixedPrice,InstanceCount,InstanceIds]" \
--output table
The State field indicates whether the RI is active, pending, or pending-fulfillment. The EndTime is calculated as StartTime + Duration.
For real-time validation, use describe-instance-status to check the status of each instance, especially during RI application:
aws ec2 describe-instance-status \
--instance-ids i-0123456789abcdef0 \
--query "InstanceStatuses[*].[InstanceId,Status.Status,Status.Details,Events]"
The Status.Status field may return ok, initializing, impaired, or scheduled. Status.Details includes Name, Status, and Message fields. A common failure mode is Status.Status = "impaired" with Status.Details.Message = "Reserved Instance not applied", which indicates the RI was purchased but not associated with the instance.
When multiple RIs apply to the same instance, the system applies them in order of StartTime. The first active RI takes precedence. If two RIs apply to the same instance, the one with the earliest StartTime is used. If StartTime is identical, the one with the smallest ReservationId is chosen.
To manage overlap, use modify-reserved-instances to adjust the InstanceCount, Duration, or InstanceType of an existing RI. The operation is asynchronous and returns a ModifyReservedInstancesResponse:
{
"ReservedInstancesId": "r-0a1b2c3d4e5f67890",
"Status": "modifying",
"StatusMessage": "Reservation modified successfully"
}
The Status can be modifying, modified, or failed. A silent failure occurs when Status is modified but StatusMessage is empty. This often arises when ModifyReservedInstances is called without InstanceCount or Duration in the request body.
When RIs are underutilized—e.g., an instance runs only 50% of the time—the cloud provider may charge the full FixedPrice and PricePerHour even when the instance is idle. To detect underutilization, compute the actual instance-hours per month and compare it to the total hours covered by the RI.
# Compute actual usage over 30 days
aws cloudwatch get-metric-statistics \
--namespace "AWS/EC2" \
--metric-name "CPUUtilization" \
--dimensions Name=InstanceId,Value=i-0123456789abcdef0 \
--start-time 2024-04-01T00:00:00Z \
--end-time 2024-05-01T00:00:00Z \
--period 86400 \
--statistics Average \
--output json
This returns Datapoints with Timestamp, Average, and Unit. Aggregate the Average values to derive total CPU utilization over time, and use it to estimate actual instance-hours.
When a workload evolves, replace an existing RI with a new one. Use exchange-reserved-instances to swap one or more RIs for a new offering. The process is initiated by sending a request with the ReservationId of the old RI and a list of TargetOfferingIds.
{
"SourceReservationId": "r-0a1b2c3d4e5f67890",
"TargetOfferingIds": [
"r-1b2c3d4e5f67890a",
"r-2c3d4e5f67890a1b"
],
"DryRun": true,
"TargetInstanceCount": 5
}
The DryRun flag is crucial: it validates the request without charging. If DryRun is true, the API returns DryRunResult with Status, StatusMessage, and ValidationErrors.
The ValidationErrors array contains Code, Message, and Field fields. A common error is Code = "InvalidTargetOfferingId" with Message = "Offering ID not found in the region", indicating the target offering is not available in the target region.
After exchange, the original RI is converted into a new reservation with updated InstanceType, Duration, and FixedPrice. The original instance continues to use the old RI until the exchange completes. The new RI applies from StartTime onward, and overlapping RIs are resolved as described.
RIs expire at EndTime. The system automatically charges the PricePerHour for the duration of the reservation. If the instance remains after EndTime, it is billed at the on-demand rate.
To renew an RI, use purchase-reserved-instances-offering with a ReservationId from an existing RI. This creates a new reservation with the same InstanceType, Duration, and Region, and the FixedPrice is applied to the new reservation.
For automatic renewal, use a scheduled job that runs one month before EndTime:
# Fetch all RIs expiring in the next 30 days
aws ec2 describe-reserved-instances \
--filters "Name=state,Values=active" \
--filters "Name=duration,Values=2920" \
--query "ReservedInstances[*].[ReservationId,InstanceType,StartTime,EndTime,FixedPrice,InstanceCount]"
The Duration is in hours. A 3-year RI has Duration = 26280, so Duration = 2920 corresponds to approximately 1 month (2920 hours ≈ 122 days). The job generates renewal orders and applies them using purchase-reserved-instances-offering.
Status.Status = "impaired" and Status.Details.Message = "Reserved Instance not applied" — often due to missing InstanceIds in associate-reserved-instances.StartTime is misaligned — the second RI starts before the first finishes.DryRun = true but ValidationErrors are empty — the request is valid, but no error is returned.describe-reserved-instances-offerings returns an offering with InstanceType = "m5.xlarge", but purchase-reserved-instances-offering fails with Code = "InvalidInstanceType" and Message = "Instance type m5.xlarge not available in region us-west-2".Duration = 2920, because Duration is stored in hours and the query uses Duration = 2920 but the actual value is 2921 — off by one hour.| Operation | HTTP Endpoint | Request Body | Response | Common Error |
|---|---|---|---|---|
describe-reserved-instances-offerings | GET /2021-01-01/ReservedInstancesOfferings | Query string filters | List of offerings | InvalidFilter |
purchase-reserved-instances-offering | POST /2021-01-01/PurchaseReservedInstances | JSON with ReservationId, InstanceCount, InstanceIds | ReservationId, StartTime, EndTime | InvalidReservationId |
associate-reserved-instances | POST /2021-01-01/AssociateReservedInstances | ReservationId, InstanceIds | Status, StatusMessage | ReservationNotActive |
modify-reserved-instances | POST /2021-01-01/ModifyReservedInstances | ReservationId, InstanceCount, Duration | Status, StatusMessage | InvalidInstanceCount |
exchange-reserved-instances | POST /2021-01-01/ExchangeReservedInstances | SourceReservationId, TargetOfferingIds | Status, StatusMessage, ValidationErrors | InvalidTargetOfferingId |
Each operation is idempotent and supports DryRun to validate before execution. The ReservationId is the central identifier linking all operations, and the StartTime and EndTime define the lifecycle of each RI.
This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy