Reserved Instance Management

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.

Planning and Sizing RIs

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.

Purchasing RIs

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.

Applying RIs to Running Workloads

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.

Monitoring and Validation

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.

Handling RI Overlap and Underutilization

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.

Replacing and Exchanging RIs

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.

Managing RI Expiration and Renewal

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.

Common Failure Modes

Summary of Key Operations

OperationHTTP EndpointRequest BodyResponseCommon Error
describe-reserved-instances-offeringsGET /2021-01-01/ReservedInstancesOfferingsQuery string filtersList of offeringsInvalidFilter
purchase-reserved-instances-offeringPOST /2021-01-01/PurchaseReservedInstancesJSON with ReservationId, InstanceCount, InstanceIdsReservationId, StartTime, EndTimeInvalidReservationId
associate-reserved-instancesPOST /2021-01-01/AssociateReservedInstancesReservationId, InstanceIdsStatus, StatusMessageReservationNotActive
modify-reserved-instancesPOST /2021-01-01/ModifyReservedInstancesReservationId, InstanceCount, DurationStatus, StatusMessageInvalidInstanceCount
exchange-reserved-instancesPOST /2021-01-01/ExchangeReservedInstancesSourceReservationId, TargetOfferingIdsStatus, StatusMessage, ValidationErrorsInvalidTargetOfferingId

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.