Performance Optimization
Performance Overview
Provider performance directly impacts user experience. Slow operations lead to frustration, increased costs, and reduced productivity.
Performance Metrics
Key Indicators
Response Time:
├── API latency
├── Network overhead
├── Processing time
└── State operations
Resource Usage:
├── Memory consumption
├── CPU utilization
├── Goroutine count
└── Network bandwidth
Operation Count:
├── API calls per operation
├── State reads/writes
├── Validation checks
└── Computed attributes
API Call Optimization
Problem: Excessive API Calls
// ❌ Bad: Individual API calls
func (r *CacheResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state CacheResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
// Multiple API calls
cache, err := r.client.GetCache(ctx, state.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("API Error", err.Error())
return
}
// Separate call for metrics
metrics, err := r.client.GetCacheMetrics(ctx, state.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("API Error", err.Error())
return
}
// Another call for configuration
config, err := r.client.GetCacheConfig(ctx, state.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("API Error", err.Error())
return
}
// Total: 3 API calls
}
Solution: Batch API Calls
// ✅ Good: Single batch call
func (r *CacheResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state CacheResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
// Single batch call
details, err := r.client.GetCacheDetails(ctx, state.ID.ValueString(), &GetOptions{
IncludeMetrics: true,
IncludeConfiguration: true,
})
if err != nil {
resp.Diagnostics.AddError("API Error", err.Error())
return
}
// Total: 1 API call (saves ~66% time)
}
Batch Client Implementation
type Client struct {
httpClient *http.Client
baseURL string
}
type GetOptions struct {
IncludeMetrics bool
IncludeConfiguration bool
IncludeTags bool
}
func (c *Client) GetCacheDetails(ctx context.Context, id string, opts *GetOptions) (*CacheDetails, error) {
// Build query parameters
params := url.Values{}
if opts.IncludeMetrics {
params.Add("include", "metrics")
}
if opts.IncludeConfiguration {
params.Add("include", "configuration")
}
if opts.IncludeTags {
params.Add("include", "tags")
}
// Single request with all data
endpoint := fmt.Sprintf("%s/caches/%s?%s", c.baseURL, id, params.Encode())
req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil)
if err != nil {
return nil, err
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var details CacheDetails
if err := json.NewDecoder(resp.Body).Decode(&details); err != nil {
return nil, err
}
return &details, nil
}
Response Caching
In-Memory Cache
type CachedClient struct {
client *Client
cache *sync.Map
ttl time.Duration
}
type CacheEntry struct {
Data interface{}
ExpiresAt time.Time
}
func NewCachedClient(client *Client, ttl time.Duration) *CachedClient {
return &CachedClient{
client: client,
cache: &sync.Map{},
ttl: ttl,
}
}
func (c *CachedClient) GetCache(ctx context.Context, id string) (*Cache, error) {
cacheKey := fmt.Sprintf("cache:%s", id)
// Check cache
if entry, found := c.cache.Load(cacheKey); found {
cached := entry.(*CacheEntry)
if time.Now().Before(cached.ExpiresAt) {
tflog.Debug(ctx, "Cache hit", map[string]interface{}{
"key": cacheKey,
})
return cached.Data.(*Cache), nil
}
// Expired, remove
c.cache.Delete(cacheKey)
}
// Fetch from API
tflog.Debug(ctx, "Cache miss", map[string]interface{}{
"key": cacheKey,
})
cache, err := c.client.GetCache(ctx, id)
if err != nil {
return nil, err
}
// Store in cache
c.cache.Store(cacheKey, &CacheEntry{
Data: cache,
ExpiresAt: time.Now().Add(c.ttl),
})
return cache, nil
}
// Clear cache when modifications occur
func (c *CachedClient) UpdateCache(ctx context.Context, id string, updates *CacheUpdates) (*Cache, error) {
cache, err := c.client.UpdateCache(ctx, id, updates)
if err != nil {
return nil, err
}
// Invalidate cache
cacheKey := fmt.Sprintf("cache:%s", id)
c.cache.Delete(cacheKey)
return cache, nil
}
Cache Configuration
func (p *provider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var config providerModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
// Create base client
baseClient := NewClient(config.APIKey.ValueString())
// Wrap with caching (5 minute TTL)
cachedClient := NewCachedClient(baseClient, 5*time.Minute)
// Make available to resources
resp.ResourceData = cachedClient
resp.DataSourceData = cachedClient
}
Parallel Operations
Concurrent API Calls
// ✅ Good: Parallel fetching
func (d *CachesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config CachesDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
// Get list of cache IDs
cacheIDs, err := d.client.ListCacheIDs(ctx)
if err != nil {
resp.Diagnostics.AddError("API Error", err.Error())
return
}
// Fetch details in parallel
type result struct {
cache *Cache
err error
}
results := make(chan result, len(cacheIDs))
var wg sync.WaitGroup
// Limit concurrency
semaphore := make(chan struct{}, 10) // Max 10 concurrent requests
for _, id := range cacheIDs {
wg.Add(1)
go func(cacheID string) {
defer wg.Done()
semaphore <- struct{}{} // Acquire
defer func() { <-semaphore }() // Release
cache, err := d.client.GetCache(ctx, cacheID)
results <- result{cache: cache, err: err}
}(id)
}
// Wait for completion
go func() {
wg.Wait()
close(results)
}()
// Collect results
var caches []*Cache
for res := range results {
if res.err != nil {
resp.Diagnostics.AddError("API Error", res.err.Error())
continue
}
caches = append(caches, res.cache)
}
// Build state
state := CachesDataSourceModel{
Caches: buildCacheModels(ctx, caches),
}
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
Error Handling with Parallel Operations
func fetchParallel(ctx context.Context, client *Client, ids []string) ([]*Cache, error) {
type result struct {
index int
cache *Cache
err error
}
results := make(chan result, len(ids))
var wg sync.WaitGroup
for i, id := range ids {
wg.Add(1)
go func(index int, cacheID string) {
defer wg.Done()
cache, err := client.GetCache(ctx, cacheID)
results <- result{
index: index,
cache: cache,
err: err,
}
}(i, id)
}
go func() {
wg.Wait()
close(results)
}()
// Collect results preserving order
caches := make([]*Cache, len(ids))
var errs []error
for res := range results {
if res.err != nil {
errs = append(errs, fmt.Errorf("cache %s: %w", ids[res.index], res.err))
continue
}
caches[res.index] = res.cache
}
if len(errs) > 0 {
return nil, fmt.Errorf("failed to fetch caches: %v", errs)
}
return caches, nil
}
Connection Pooling
HTTP Client Configuration
func NewOptimizedClient(apiKey string) *Client {
// Configure transport with pooling
transport := &http.Transport{
MaxIdleConns: 100, // Total idle connections
MaxIdleConnsPerHost: 10, // Idle per host
MaxConnsPerHost: 50, // Max per host
IdleConnTimeout: 90 * time.Second, // Idle timeout
TLSHandshakeTimeout: 10 * time.Second,
DisableCompression: false,
DisableKeepAlives: false, // Enable keep-alive
}
httpClient := &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
}
return &Client{
httpClient: httpClient,
apiKey: apiKey,
}
}
Connection Reuse
// ✅ Good: Reuse connections
func (c *Client) makeRequest(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, body)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+c.apiKey)
req.Header.Set("User-Agent", "terraform-provider-cachecloud/1.0")
// Connection pooling happens automatically
return c.httpClient.Do(req)
}
Memory Optimization
Efficient State Handling
// ❌ Bad: Copying entire state
func (r *CacheResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan, state CacheResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
// Full copy of potentially large state
newState := state
newState.Name = plan.Name
newState.Config = plan.Config
// ...
}
// ✅ Good: Modify in place
func (r *CacheResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan, state CacheResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
// Only update changed fields
if !plan.Name.Equal(state.Name) {
state.Name = plan.Name
}
if !plan.Config.Equal(state.Config) {
state.Config = plan.Config
}
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
Stream Large Responses
// ✅ Good: Stream and process incrementally
func (c *Client) ExportLargeDataset(ctx context.Context, id string) error {
resp, err := c.httpClient.Get(c.baseURL + "/export/" + id)
if err != nil {
return err
}
defer resp.Body.Close()
// Stream process without loading entire response
scanner := bufio.NewScanner(resp.Body)
scanner.Buffer(make([]byte, 64*1024), 1024*1024) // 1MB buffer
for scanner.Scan() {
line := scanner.Bytes()
// Process line incrementally
if err := processLine(line); err != nil {
return err
}
}
return scanner.Err()
}
Computed Attribute Optimization
Selective Computation
schema.Int64Attribute{
Description: "Total size in bytes",
Computed: true,
// Only recompute when these attributes change
PlanModifiers: []planmodifier.Int64{
int64planmodifier.UseStateForUnknown(), // Skip if plan is unknown
},
}
Efficient Default Values
// ✅ Good: Static default
schema.StringAttribute{
Optional: true,
Computed: true,
Default: stringdefault.StaticString("cache.t3.micro"),
}
// ❌ Bad: Dynamic default requiring computation
schema.StringAttribute{
Optional: true,
Computed: true,
Default: stringdefault.UseStateForUnknown(), // Recomputed every time
}
Benchmark Testing
Basic Benchmark
func BenchmarkCacheRead(b *testing.B) {
ctx := context.Background()
client := NewClient("test-key")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := client.GetCache(ctx, "test-cache-id")
if err != nil {
b.Fatal(err)
}
}
}
Parallel Benchmark
func BenchmarkCacheReadParallel(b *testing.B) {
ctx := context.Background()
client := NewClient("test-key")
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, err := client.GetCache(ctx, "test-cache-id")
if err != nil {
b.Fatal(err)
}
}
})
}
Memory Allocation Benchmark
func BenchmarkStateUpdate(b *testing.B) {
ctx := context.Background()
b.ReportAllocs() // Report memory allocations
b.ResetTimer()
for i := 0; i < b.N; i++ {
state := CacheResourceModel{
ID: types.StringValue("test-id"),
Name: types.StringValue("test-name"),
}
// Simulate state update
state.Name = types.StringValue("new-name")
}
}
Running Benchmarks
# Run all benchmarks
go test -bench=. -benchmem
# Output:
# BenchmarkCacheRead-8 1000 1234567 ns/op 4096 B/op 10 allocs/op
# BenchmarkCacheReadParallel-8 5000 246813 ns/op 4096 B/op 10 allocs/op
# BenchmarkStateUpdate-8 10000000 123 ns/op 64 B/op 2 allocs/op
# Run specific benchmark
go test -bench=BenchmarkCacheRead -benchtime=10s
# CPU profiling
go test -bench=. -cpuprofile=cpu.prof
go tool pprof cpu.prof
# Memory profiling
go test -bench=. -memprofile=mem.prof
go tool pprof mem.prof
Profiling Production Code
CPU Profiling
import (
"os"
"runtime/pprof"
)
func profileCPU() func() {
f, err := os.Create("cpu.prof")
if err != nil {
log.Fatal(err)
}
pprof.StartCPUProfile(f)
return func() {
pprof.StopCPUProfile()
f.Close()
}
}
// Usage in tests
func TestComplexOperation(t *testing.T) {
stop := profileCPU()
defer stop()
// Run operation
complexOperation()
}
Memory Profiling
func profileMemory() {
f, err := os.Create("mem.prof")
if err != nil {
log.Fatal(err)
}
defer f.Close()
runtime.GC() // Force GC
if err := pprof.WriteHeapProfile(f); err != nil {
log.Fatal(err)
}
}
Monitoring Performance
Request Duration Metrics
type MetricsMiddleware struct {
next http.RoundTripper
}
func (m *MetricsMiddleware) RoundTrip(req *http.Request) (*http.Response, error) {
start := time.Now()
resp, err := m.next.RoundTrip(req)
duration := time.Since(start)
tflog.Debug(req.Context(), "API request completed",
map[string]interface{}{
"method": req.Method,
"url": req.URL.String(),
"duration_ms": duration.Milliseconds(),
"status": resp.StatusCode,
})
return resp, err
}
Performance Logging
func (r *CacheResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
start := time.Now()
defer func() {
tflog.Debug(ctx, "Create operation completed",
map[string]interface{}{
"duration_ms": time.Since(start).Milliseconds(),
})
}()
// Implementation...
}
Best Practices
1. Batch Operations
// ✅ Good: Single batch call
items, err := client.BatchGet(ctx, ids)
// ❌ Bad: Individual calls in loop
for _, id := range ids {
item, err := client.Get(ctx, id)
}
2. Use Caching Wisely
// ✅ Good: Cache immutable data
cachedClient.GetAccountInfo(ctx) // Account info rarely changes
// ❌ Bad: Cache frequently changing data
cachedClient.GetCurrentMetrics(ctx) // Metrics change constantly
3. Limit Concurrency
// ✅ Good: Controlled concurrency
semaphore := make(chan struct{}, 10) // Max 10 concurrent
// ❌ Bad: Unbounded concurrency
for _, id := range thousandsOfIDs {
go fetch(id) // Can overwhelm system
}
4. Profile Before Optimizing
# ✅ Good: Measure first
go test -bench=. -cpuprofile=cpu.prof
# Identify bottlenecks
# Then optimize
# ❌ Bad: Premature optimization
# Optimize without measuring
Key Takeaways
- ✅ Batch API calls to reduce latency
- ✅ Implement caching for immutable data
- ✅ Use parallel operations with limits
- ✅ Configure connection pooling
- ✅ Optimize memory usage
- ✅ Benchmark critical paths
- ✅ Profile to find bottlenecks
- ✅ Monitor performance in production
What’s Next?
Learn advanced state management including migrations and upgrades.
Next: Advanced State Management →
Previous: ← Complex Attribute Types
Next: Advanced State Management →
Last updated: July 1, 2026