Resource Timeouts

8 minute read

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-timeouts package
  • ✅ Define timeouts in schema Blocks section
  • ✅ Get timeout values in CRUD operations with defaults
  • ✅ Create context with timeout for operations
  • ✅ Check for context.DeadlineExceeded errors
  • ✅ 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

Updated: