Resource Timeouts
What are Resource Timeouts?
Resource timeouts allow users to customize how long Terraform should wait for operations to complete. This is essential for long-running operations like database creation or large instance resizing.
Why timeouts matter:
- ✅ Different resources take different amounts of time
- ✅ Users can adjust based on their infrastructure
- ✅ Prevents indefinite waiting
- ✅ Provides control over operation duration
- ✅ Better error handling for slow operations
User experience:
resource "mycloud_database" "example" {
name = "my-database"
timeouts {
create = "45m"
update = "30m"
delete = "20m"
}
}
Timeout Schema
Define timeout configuration in your resource schema:
import (
"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
)
type DatabaseResourceModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
// ... other attributes ...
Timeouts timeouts.Value `tfsdk:"timeouts"`
}
func (r *DatabaseResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
},
"name": schema.StringAttribute{
Required: true,
},
// ... other attributes ...
},
Blocks: map[string]schema.Block{
"timeouts": timeouts.Block(ctx, timeouts.Opts{
Create: true,
Update: true,
Delete: true,
Read: false, // Read typically doesn't need timeout
}),
},
}
}
Using Timeouts in Operations
Create Operation
func (r *DatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan DatabaseResourceModel
req.Plan.Get(ctx, &plan)
// Get create timeout
createTimeout, diags := plan.Timeouts.Create(ctx, 30*time.Minute) // Default 30m
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Create context with timeout
createCtx, cancel := context.WithTimeout(ctx, createTimeout)
defer cancel()
tflog.Info(ctx, "Creating database", map[string]any{
"timeout": createTimeout.String(),
})
// Start creation
db, err := r.client.CreateDatabase(createCtx, api.CreateDatabaseRequest{
Name: plan.Name.ValueString(),
Engine: plan.Engine.ValueString(),
})
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
resp.Diagnostics.AddError(
"Create Timeout",
fmt.Sprintf(
"Database creation did not complete within %s.\n\n"+
"The database may still be creating. Check the console.\n"+
"You can increase the timeout:\n"+
" timeouts {\n"+
" create = \"60m\"\n"+
" }",
createTimeout,
),
)
return
}
resp.Diagnostics.AddError("Create Failed", err.Error())
return
}
// Wait for database to be ready with timeout
err = r.waitForDatabaseReady(createCtx, db.ID)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
resp.Diagnostics.AddError(
"Create Timeout",
fmt.Sprintf("Database did not become ready within %s", createTimeout),
)
return
}
resp.Diagnostics.AddError("Wait Failed", err.Error())
return
}
// Set state
plan.ID = types.StringValue(db.ID)
resp.State.Set(ctx, plan)
}
Update Operation
func (r *DatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan, state DatabaseResourceModel
req.Plan.Get(ctx, &plan)
req.State.Get(ctx, &state)
// Get update timeout
updateTimeout, diags := plan.Timeouts.Update(ctx, 20*time.Minute) // Default 20m
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Create context with timeout
updateCtx, cancel := context.WithTimeout(ctx, updateTimeout)
defer cancel()
tflog.Info(ctx, "Updating database", map[string]any{
"timeout": updateTimeout.String(),
})
// Perform update
err := r.client.UpdateDatabase(updateCtx, state.ID.ValueString(), api.UpdateDatabaseRequest{
InstanceClass: plan.InstanceClass.ValueString(),
StorageGB: int(plan.StorageGB.ValueInt64()),
})
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
resp.Diagnostics.AddError(
"Update Timeout",
fmt.Sprintf("Database update did not complete within %s", updateTimeout),
)
return
}
resp.Diagnostics.AddError("Update Failed", err.Error())
return
}
// Wait for update to complete
err = r.waitForDatabaseReady(updateCtx, state.ID.ValueString())
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
resp.Diagnostics.AddError(
"Update Timeout",
"Database update did not complete in time",
)
return
}
resp.Diagnostics.AddError("Wait Failed", err.Error())
return
}
resp.State.Set(ctx, plan)
}
Delete Operation
func (r *DatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state DatabaseResourceModel
req.State.Get(ctx, &state)
// Get delete timeout
deleteTimeout, diags := state.Timeouts.Delete(ctx, 15*time.Minute) // Default 15m
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Create context with timeout
deleteCtx, cancel := context.WithTimeout(ctx, deleteTimeout)
defer cancel()
tflog.Info(ctx, "Deleting database", map[string]any{
"timeout": deleteTimeout.String(),
})
// Delete database
err := r.client.DeleteDatabase(deleteCtx, state.ID.ValueString())
if err != nil && !api.IsNotFoundError(err) {
if errors.Is(err, context.DeadlineExceeded) {
resp.Diagnostics.AddError(
"Delete Timeout",
fmt.Sprintf("Database deletion did not complete within %s", deleteTimeout),
)
return
}
resp.Diagnostics.AddError("Delete Failed", err.Error())
return
}
// Wait for deletion to complete
err = r.waitForDatabaseDeleted(deleteCtx, state.ID.ValueString())
if err != nil && !api.IsNotFoundError(err) {
if errors.Is(err, context.DeadlineExceeded) {
resp.Diagnostics.AddWarning(
"Delete Timeout",
fmt.Sprintf(
"Database deletion did not complete within %s.\n"+
"The deletion may still be in progress.",
deleteTimeout,
),
)
// Continue - let Terraform remove from state
}
}
}
Waiting with Timeouts
Polling Pattern
func (r *DatabaseResource) waitForDatabaseReady(ctx context.Context, id string) error {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
// Timeout or cancellation
return ctx.Err()
case <-ticker.C:
// Check status
db, err := r.client.GetDatabase(ctx, id)
if err != nil {
return err
}
tflog.Debug(ctx, "Waiting for database", map[string]any{
"id": id,
"status": db.Status,
})
if db.Status == "available" {
return nil
}
if db.Status == "failed" {
return fmt.Errorf("database entered failed state")
}
// Continue waiting
}
}
}
Exponential Backoff
func (r *DatabaseResource) waitForDatabaseReady(ctx context.Context, id string) error {
backoff := 2 * time.Second
maxBackoff := 30 * time.Second
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(backoff):
db, err := r.client.GetDatabase(ctx, id)
if err != nil {
return err
}
if db.Status == "available" {
return nil
}
if db.Status == "failed" {
return fmt.Errorf("database failed")
}
// Increase backoff
backoff = backoff * 2
if backoff > maxBackoff {
backoff = maxBackoff
}
}
}
}
Default Timeouts
Set sensible defaults based on operation type:
func (r *DatabaseResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Blocks: map[string]schema.Block{
"timeouts": timeouts.Block(ctx, timeouts.Opts{
Create: true, // Default in Create: 30m
Update: true, // Default in Update: 20m
Delete: true, // Default in Delete: 15m
}),
},
}
}
// In CRUD operations, specify defaults
createTimeout, _ := plan.Timeouts.Create(ctx, 30*time.Minute)
updateTimeout, _ := plan.Timeouts.Update(ctx, 20*time.Minute)
deleteTimeout, _ := state.Timeouts.Delete(ctx, 15*time.Minute)
Operation-Specific Timeouts
Different operations may need different defaults:
func (r *DatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan DatabaseResourceModel
req.Plan.Get(ctx, &plan)
// Determine timeout based on operation complexity
defaultTimeout := 30 * time.Minute
// Large databases take longer
if plan.StorageGB.ValueInt64() > 1000 {
defaultTimeout = 60 * time.Minute
}
// Multi-AZ deployment takes longer
if !plan.MultiAZ.IsNull() && plan.MultiAZ.ValueBool() {
defaultTimeout = 45 * time.Minute
}
createTimeout, _ := plan.Timeouts.Create(ctx, defaultTimeout)
// Use timeout...
}
Timeout Error Messages
Provide helpful messages when timeouts occur:
if errors.Is(err, context.DeadlineExceeded) {
resp.Diagnostics.AddError(
"Create Operation Timeout",
fmt.Sprintf(
"Database creation did not complete within the configured timeout of %s.\n\n"+
"The database may still be creating in the background. Check your console.\n\n"+
"To increase the timeout, add to your resource configuration:\n"+
" timeouts {\n"+
" create = \"60m\" # Increase as needed\n"+
" }\n\n"+
"Factors that affect creation time:\n"+
" - Database size (storage_gb)\n"+
" - Multi-AZ deployment\n"+
" - Region capacity\n"+
" - Number of replicas",
createTimeout,
),
)
return
}
Timeout Best Practices
1. Document Default Timeouts
"timeouts": timeouts.Block(ctx, timeouts.Opts{
Create: true, // Default: 30 minutes
Update: true, // Default: 20 minutes
Delete: true, // Default: 15 minutes
}),
In documentation:
## Timeouts
`mycloud_database` provides the following timeouts:
- `create` - (Default 30 minutes) How long to wait for database creation
- `update` - (Default 20 minutes) How long to wait for database updates
- `delete` - (Default 15 minutes) How long to wait for database deletion
Example:
```hcl
resource "mycloud_database" "example" {
# ...
timeouts {
create = "45m"
update = "30m"
delete = "20m"
}
}
### 2. Use Context Throughout
```go
// ✅ Good: Pass context to all API calls
db, err := r.client.CreateDatabase(ctx, req)
err = r.waitForReady(ctx, db.ID)
// ❌ Bad: Using background context
db, err := r.client.CreateDatabase(context.Background(), req)
3. Check Context Cancellation
// ✅ Good: Check for cancellation
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
// Continue polling
}
// ❌ Bad: Infinite loop without checking context
for {
time.Sleep(5 * time.Second)
// Never checks if cancelled!
}
4. Provide Realistic Defaults
// ✅ Good: Based on actual operation times
createTimeout, _ := plan.Timeouts.Create(ctx, 30*time.Minute)
// ❌ Bad: Unrealistic defaults
createTimeout, _ := plan.Timeouts.Create(ctx, 5*time.Minute) // Too short!
5. Log Timeout Information
// ✅ Good: Log timeout configuration
tflog.Debug(ctx, "Starting operation", map[string]any{
"operation": "create",
"timeout": createTimeout.String(),
})
Complete Example
func (r *DatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
// 1. Get plan
var plan DatabaseResourceModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// 2. Get create timeout with default
createTimeout, diags := plan.Timeouts.Create(ctx, 30*time.Minute)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// 3. Create context with timeout
createCtx, cancel := context.WithTimeout(ctx, createTimeout)
defer cancel()
// 4. Log operation with timeout
tflog.Info(ctx, "Creating database", map[string]any{
"name": plan.Name.ValueString(),
"timeout": createTimeout.String(),
})
// 5. Start creation
db, err := r.client.CreateDatabase(createCtx, api.CreateDatabaseRequest{
Name: plan.Name.ValueString(),
Engine: plan.Engine.ValueString(),
EngineVersion: plan.EngineVersion.ValueString(),
InstanceClass: plan.InstanceClass.ValueString(),
StorageGB: int(plan.StorageGB.ValueInt64()),
})
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
resp.Diagnostics.AddError(
"Create Timeout",
fmt.Sprintf(
"Database creation did not complete within %s.\n\n"+
"To increase the timeout:\n"+
" timeouts {\n"+
" create = \"60m\"\n"+
" }",
createTimeout,
),
)
return
}
resp.Diagnostics.AddError("Create Failed", err.Error())
return
}
// 6. Wait for ready state
tflog.Debug(ctx, "Waiting for database to become ready")
err = r.waitForDatabaseReady(createCtx, db.ID)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
resp.Diagnostics.AddError(
"Create Timeout",
fmt.Sprintf(
"Database did not become ready within %s.\n"+
"Database ID: %s\n"+
"Check the console for current status.",
createTimeout, db.ID,
),
)
return
}
resp.Diagnostics.AddError(
"Failed to Wait for Database",
err.Error(),
)
return
}
// 7. Fetch final state
db, err = r.client.GetDatabase(createCtx, db.ID)
if err != nil {
resp.Diagnostics.AddError("Read After Create Failed", err.Error())
return
}
// 8. Set state
plan.ID = types.StringValue(db.ID)
plan.Status = types.StringValue(db.Status)
plan.Endpoint = types.StringValue(db.Endpoint)
plan.CreatedAt = types.StringValue(db.CreatedAt)
diags = resp.State.Set(ctx, plan)
resp.Diagnostics.Append(diags...)
tflog.Info(ctx, "Database created successfully", map[string]any{
"id": db.ID,
"duration": time.Since(start).String(),
})
}
func (r *DatabaseResource) waitForDatabaseReady(ctx context.Context, id string) error {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
db, err := r.client.GetDatabase(ctx, id)
if err != nil {
if api.IsNotFoundError(err) {
return fmt.Errorf("database disappeared during creation")
}
return err
}
tflog.Trace(ctx, "Database status", map[string]any{
"id": id,
"status": db.Status,
})
switch db.Status {
case "available":
return nil
case "failed", "deleting":
return fmt.Errorf("database entered %s state", db.Status)
case "creating", "modifying", "backing-up":
// Continue waiting
default:
tflog.Warn(ctx, "Unknown database status", map[string]any{
"status": db.Status,
})
}
}
}
}
Key Takeaways
- ✅ Use
terraform-plugin-framework-timeoutspackage - ✅ Define timeouts in schema Blocks section
- ✅ Get timeout values in CRUD operations with defaults
- ✅ Create context with timeout for operations
- ✅ Check for
context.DeadlineExceedederrors - ✅ Use polling with context cancellation checking
- ✅ Provide helpful timeout error messages
- ✅ Set realistic default timeouts
- ✅ Document timeout options in provider docs
- ✅ Pass context to all API calls and wait functions
What’s Next?
Now let’s put everything together in a comprehensive hands-on lab!
Next: Resource Implementation Lab →
Previous: ← Resource Import
Last updated: June 30, 2026