Repository: buildkite/terraform-provider-buildkite
Pull request #901: feat: add rate limit handling for REST API calls
Author: login: scadu
Created: 2025-04-07T14:42:15Z | Merged: 2025-04-09T15:46:59Z

This PR implements rate limit handling for REST API calls using the `hashicorp/go-retryablehttp`. 
Includes:
  - Backoff using [RateLimit-Reset](https://buildkite.com/docs/apis/rest-api/limits#checking-rate-limit-details) header to time retry attempts
  - Retry configuration with 10 max attempts, 15-180s wait window defined now — can be exposed as configurable parameters later
  - Better error handling with response body content in error messages
  - Proper timeout context management for all requests

Also, added basic [Lefthook](https://github.com/evilmartians/lefthook) pre-commit check. 


Fixes https://github.com/buildkite/terraform-provider-buildkite/issues/477


Diff:
diff --git a/buildkite/client.go b/buildkite/client.go
index f6c26e63..072ed7d6 100644
--- a/buildkite/client.go
+++ b/buildkite/client.go
@@ -9,9 +9,12 @@ import (
 	"net/http"
 	"regexp"
 	"strconv"
+	"time"
 
 	genqlient "github.com/Khan/genqlient/graphql"
+	"github.com/hashicorp/go-retryablehttp"
 	"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
+	"github.com/hashicorp/terraform-plugin-log/tflog"
 	"github.com/shurcooL/graphql"
 )
 
@@ -53,26 +56,119 @@ func (client Client) GetOrganizationID() (*string, error) {
 	return client.organizationId, nil
 }
 
-// NewClient creates a client to use for interacting with the Buildkite API
+// NewClient creates a client for interacting with the Buildkite API.
+//
+// https://buildkite.com/docs/apis/rest-api/limits
+//
+// For REST API calls:
+//  1. Uses hashicorp/go-retryablehttp to provide automatic retries with smart backoff
+//  2. Maximum of 10 retry attempts for requests that fail with retryable errors
+//  3. Rate limiting strategy:
+//     - Checks RateLimit-Reset header to determine when the rate limit will be reset
+//     - Waits until the reset time plus a small buffer before retrying
+//     - Falls back to Retry-After header if reset time isn't available
+//  4. Also retries server errors (HTTP 500-599) with linear jitter backoff
+//  5. All retryable requests have a minimum wait of 15 seconds and maximum of 180 seconds
+
 func NewClient(config *clientConfig) *Client {
-	// Setup a HTTP Client that can be used by all REST and graphql API calls,
-	// with suitable headers for authentication and user agent identification
-	rt := http.DefaultTransport
+	// Create standard HTTP client for GraphQL
+	standardHttpClient := &http.Client{}
+
+	// Set timeout if configured
+	readTimeout, diags := config.timeouts.Read(context.Background(), DefaultTimeout)
+	if !diags.HasError() && readTimeout > 0 {
+		standardHttpClient.Timeout = readTimeout
+	}
+
+	// Set up authentication and user agent headers
 	header := make(http.Header)
 	header.Set("Authorization", "Bearer "+config.apiToken)
 	header.Set("User-Agent", config.userAgent)
-	rt = newHeaderRoundTripper(rt, header)
+	standardHttpClient.Transport = newHeaderRoundTripper(standardHttpClient.Transport, header)
+
+	// Create retryable client with rate limit handling for REST API calls
+	retryClient := retryablehttp.NewClient()
 
-	httpClient := &http.Client{
-		Transport: rt,
+	retryClient.RetryMax = 10
+	retryClient.RetryWaitMin = 15 * time.Second
+	retryClient.RetryWaitMax = 180 * time.Second
+	retryClient.Logger = nil
+
+	if !diags.HasError() && readTimeout > 0 {
+		retryClient.HTTPClient.Timeout = readTimeout
 	}
 
-	graphqlClient := graphql.NewClient(config.graphqlURL, httpClient)
+	// Use LinearJitterBackoff with RateLimit-Reset header support for better distribution of requests
+	retryClient.Backoff = func(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
+		if resp != nil && resp.StatusCode == http.StatusTooManyRequests {
+			// Try to use RateLimit-Reset header first
+			if resetHeader := resp.Header.Get("RateLimit-Reset"); resetHeader != "" {
+				if resetTime, err := strconv.ParseInt(resetHeader, 10, 64); err == nil {
+					resetAt := time.Unix(resetTime, 0)
+					// Add a 2-second buffer to ensure we're past the reset time
+					waitTime := time.Until(resetAt) + (2 * time.Second)
+					tflog.Debug(context.Background(), fmt.Sprintf("Rate limit hit, reset at: %v (waiting: %v)", resetAt, waitTime))
+
+					// Return the wait time within min-max bounds
+					if waitTime < min {
+						return min
+					}
+					if waitTime > max {
+						return max
+					}
+					return waitTime
+				}
+			}
+
+			// Fall back to Retry-After header if available
+			if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
+				if seconds, err := strconv.ParseInt(retryAfter, 10, 64); err == nil {
+					waitTime := time.Duration(seconds) * time.Second
+					tflog.Debug(context.Background(), fmt.Sprintf("Rate limit hit, retry after: %v", waitTime))
+
+					// Return the wait time within min-max bounds
+					if waitTime < min {
+						return min
+					}
+					if waitTime > max {
+						return max
+					}
+					return waitTime
+				}
+			}
+		}
+
+		// Use linear backoff with jitter to spread out requests when retrying
+		return retryablehttp.LinearJitterBackoff(min, max, attemptNum, resp)
+	}
+
+	retryClient.CheckRetry = func(ctx context.Context, resp *http.Response, err error) (bool, error) {
+		if err != nil || resp == nil {
+			return retryablehttp.DefaultRetryPolicy(ctx, resp, err)
+		}
+
+		if resp.StatusCode == http.StatusTooManyRequests || (resp.StatusCode >= 500 && resp.StatusCode < 600) {
+			remaining := resp.Header.Get("RateLimit-Remaining")
+			reset := resp.Header.Get("RateLimit-Reset")
+			tflog.Debug(ctx, fmt.Sprintf("Buildkite API returned %d - retrying (Remaining: %s, Reset: %s)",
+				resp.StatusCode, remaining, reset))
+			return true, nil
+		}
+
+		return false, nil
+	}
+
+	// Apply the same headers to retry client
+	retryClient.HTTPClient.Transport = newHeaderRoundTripper(retryClient.HTTPClient.Transport, header)
+	restHttpClient := retryClient.StandardClient()
+
+	// Create GraphQL client with standard HTTP client (no rate limit handling)
+	graphqlClient := graphql.NewClient(config.graphqlURL, standardHttpClient)
 
 	return &Client{
 		graphql:        graphqlClient,
-		genqlient:      genqlient.NewClient(config.graphqlURL, httpClient),
-		http:           httpClient,
+		genqlient:      genqlient.NewClient(config.graphqlURL, standardHttpClient),
+		http:           restHttpClient, // For REST API calls with rate limit handling
 		organization:   config.org,
 		organizationId: nil,
 		restURL:        config.restURL,
@@ -80,6 +176,27 @@ func NewClient(config *clientConfig) *Client {
 	}
 }
 
+func newHeaderRoundTripper(next http.RoundTripper, header http.Header) *headerRoundTripper {
+	if next == nil {
+		next = http.DefaultTransport
+	}
+	return &headerRoundTripper{
+		next:   next,
+		Header: header,
+	}
+}
+
+func (rt *headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+	if rt.Header != nil {
+		for k, v := range rt.Header {
+			req.Header[k] = v
+		}
+	}
+	return rt.next.RoundTrip(req)
+}
+
+// GraphQL-specific retry functions
+
 func isRetryableError(err error) bool {
 	return isRateLimited(err) || isServerError(err)
 }
@@ -108,27 +225,17 @@ func isServerError(err error) bool {
 	return false
 }
 
-func newHeaderRoundTripper(next http.RoundTripper, header http.Header) *headerRoundTripper {
-	if next == nil {
-		next = http.DefaultTransport
-	}
-	return &headerRoundTripper{
-		next:   next,
-		Header: header,
-	}
-}
+// NOTE: retryContextError function is defined in util.go and used for GraphQL retries
 
-func (rt *headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
-	if rt.Header != nil {
-		for k, v := range rt.Header {
-			req.Header[k] = v
-		}
+func (client *Client) makeRequest(ctx context.Context, method string, path string, postData interface{}, responseObject interface{}) error {
+	readTimeout, diags := client.timeouts.Read(ctx, DefaultTimeout)
+	if !diags.HasError() {
+		var cancel context.CancelFunc
+		ctx, cancel = context.WithTimeout(ctx, readTimeout)
+		defer cancel()
 	}
-	return rt.next.RoundTrip(req)
-}
 
-func (client *Client) makeRequest(ctx context.Context, method string, path string, postData interface{}, responseObject interface{}) error {
-	var bodyBytes io.Reader
+	bodyBytes := io.Reader(nil)
 	if postData != nil {
 		jsonPayload, err := json.Marshal(postData)
 		if err != nil {
@@ -137,22 +244,44 @@ func (client *Client) makeRequest(ctx context.Context, method string, path strin
 		bodyBytes = bytes.NewBuffer(jsonPayload)
 	}
 
-	url := fmt.Sprintf("%s%s", client.restURL, path)
+	var url = fmt.Sprintf("%s%s", client.restURL, path)
 
 	req, err := http.NewRequestWithContext(ctx, method, url, bodyBytes)
 	if err != nil {
 		return fmt.Errorf("failed to create request: %w", err)
 	}
 
+	// Add content-type header for POST/PUT requests with body
+	if (method == http.MethodPost || method == http.MethodPut) && bodyBytes != nil {
+		req.Header.Set("Content-Type", "application/json")
+	}
+
 	resp, err := client.http.Do(req)
 	if err != nil {
 		return fmt.Errorf("failed to send request: %w", err)
 	}
+
 	if resp.StatusCode >= 400 {
-		return fmt.Errorf("the Buildkite API request failed: %s %s (returned error %d)", method, url, resp.StatusCode)
+		defer resp.Body.Close()
+
+		// Try to read the error body for better error messages
+		var errorMsg string
+		errorBody, readErr := io.ReadAll(resp.Body)
+		if readErr == nil && len(errorBody) > 0 {
+			errorMsg = string(errorBody)
+		}
+
+		if errorMsg != "" {
+			return fmt.Errorf("the Buildkite API request failed: %s %s (status: %d): %s",
+				method, url, resp.StatusCode, errorMsg)
+		}
+		return fmt.Errorf("the Buildkite API request failed: %s %s (status: %d)",
+			method, url, resp.StatusCode)
 	} else if resp.StatusCode == 204 {
+		resp.Body.Close()
 		return nil
 	}
+
 	defer resp.Body.Close()
 
 	responseBody, err := io.ReadAll(resp.Body)
diff --git a/buildkite/data_source_cluster.go b/buildkite/data_source_cluster.go
index 4f4c14a8..3c688226 100644
--- a/buildkite/data_source_cluster.go
+++ b/buildkite/data_source_cluster.go
@@ -7,7 +7,6 @@ import (
 	"github.com/hashicorp/terraform-plugin-framework/datasource"
 	"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
 	"github.com/hashicorp/terraform-plugin-framework/types"
-	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
 )
 
 type clusterDatasource struct {
@@ -38,56 +37,50 @@ func (c *clusterDatasource) Read(ctx context.Context, req datasource.ReadRequest
 		return
 	}
 
-	timeout, diags := c.client.timeouts.Read(ctx, DefaultTimeout)
+	_, diags := c.client.timeouts.Read(ctx, DefaultTimeout)
 	resp.Diagnostics.Append(diags...)
 	if resp.Diagnostics.HasError() {
 		return
 	}
 
-	var matchFound bool
-	err := retry.RetryContext(ctx, timeout, func() *retry.RetryError {
-		var cursor *string
-		for {
-			r, err := getClusterByName(ctx, c.client.genqlient, c.client.organization, cursor)
-			if err != nil {
-				if isRetryableError(err) {
-					return retry.RetryableError(err)
-				}
-				resp.Diagnostics.AddError(
-					"Unable to read Cluster",
-					fmt.Sprintf("Unable to read Cluster: %s", err.Error()),
-				)
-				return retry.NonRetryableError(err)
-			}
-
-			// loop over this page of results to try find the matching cluster
-			for _, cluster := range r.Organization.Clusters.Edges {
-				if cluster.Node.Name == state.Name.ValueString() {
-					matchFound = true
-					state.Color = types.StringPointerValue(cluster.Node.Color)
-					state.Description = types.StringPointerValue(cluster.Node.Description)
-					state.Emoji = types.StringPointerValue(cluster.Node.Emoji)
-					state.ID = types.StringValue(cluster.Node.Id)
-					state.Name = types.StringValue(cluster.Node.Name)
-					state.UUID = types.StringValue(cluster.Node.Uuid)
-					break
-				}
-			}
+	var r *getClusterByNameResponse
+	var err error
+	cursor := (*string)(nil)
+	matchFound := false
+
+	// Loop through all pages until a match is found or we run out of pages
+	for {
+		r, err = getClusterByName(ctx, c.client.genqlient, c.client.organization, cursor)
+		if err != nil {
+			resp.Diagnostics.AddError(
+				"Unable to read Cluster",
+				fmt.Sprintf("Unable to read Cluster: %s", err.Error()),
+			)
+			return
+		}
 
-			// end here if we found a match or there are no more pages to search
-			if matchFound || !r.Organization.Clusters.PageInfo.HasNextPage {
+		// loop over this page of results to try find the matching cluster
+		for _, cluster := range r.Organization.Clusters.Edges {
+			if cluster.Node.Name == state.Name.ValueString() {
+				matchFound = true
+				state.Color = types.StringPointerValue(cluster.Node.Color)
+				state.Description = types.StringPointerValue(cluster.Node.Description)
+				state.Emoji = types.StringPointerValue(cluster.Node.Emoji)
+				state.ID = types.StringValue(cluster.Node.Id)
+				state.Name = types.StringValue(cluster.Node.Name)
+				state.UUID = types.StringValue(cluster.Node.Uuid)
 				break
 			}
-			cursor = &r.Organization.Clusters.PageInfo.EndCursor
 		}
-		return nil
-	})
-	if err != nil {
-		resp.Diagnostics.AddError("Unable to find Cluster", err.Error())
-		return
+
+		// end here if we found a match or there are no more pages to search
+		if matchFound || !r.Organization.Clusters.PageInfo.HasNextPage {
+			break
+		}
+		cursor = &r.Organization.Clusters.PageInfo.EndCursor
 	}
 
-	// if there is no match found by here then the cluster doesn't exist
+	// If there is no match found by here then the cluster doesn't exist
 	if !matchFound {
 		resp.Diagnostics.AddError("Unable to find Cluster", fmt.Sprintf("Could not find cluster with name \"%s\"", state.Name.ValueString()))
 		return
diff --git a/buildkite/data_source_cluster_test.go b/buildkite/data_source_cluster_test.go
index d57b14e1..6d870bb2 100644
--- a/buildkite/data_source_cluster_test.go
+++ b/buildkite/data_source_cluster_test.go
@@ -29,8 +29,7 @@ func TestAccBuildkiteClusterDatasource(t *testing.T) {
 						data "buildkite_cluster" "default" {
 							name = buildkite_cluster.cluster.name
 						}`, clusterName),
-					ExpectError: regexp.MustCompile(`timeout while waiting for state to become 'success'`),
-				},
+					ExpectError: regexp.MustCompile(`timeout while waiting for state to become 'success'`)},
 			},
 		})
 	})
diff --git a/buildkite/data_source_meta.go b/buildkite/data_source_meta.go
index f15b5e16..58fe3eee 100644
--- a/buildkite/data_source_meta.go
+++ b/buildkite/data_source_meta.go
@@ -40,8 +40,8 @@ func (*metaDatasource) Metadata(ctx context.Context, req datasource.MetadataRequ
 
 func (m *metaDatasource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
 	var state metaDatasourceModel
-	meta := MetaResponse{}
-	err := m.client.makeRequest(ctx, "GET", "/v2/meta", nil, &meta)
+	var meta MetaResponse
+	var err = m.client.makeRequest(ctx, "GET", "/v2/meta", nil, &meta)
 	if err != nil {
 		resp.Diagnostics.AddError(
 			"Failed to read meta",
diff --git a/buildkite/data_source_pipeline_template.go b/buildkite/data_source_pipeline_template.go
index b2c3863a..318ecc00 100644
--- a/buildkite/data_source_pipeline_template.go
+++ b/buildkite/data_source_pipeline_template.go
@@ -11,7 +11,6 @@ import (
 	"github.com/hashicorp/terraform-plugin-framework/path"
 	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
 	"github.com/hashicorp/terraform-plugin-framework/types"
-	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
 )
 
 type pipelineTemplateDatasourceModel struct {
@@ -96,7 +95,7 @@ func (pt *pipelineTemplateDatasource) Read(ctx context.Context, req datasource.R
 		return
 	}
 
-	timeouts, diags := pt.client.timeouts.Read(ctx, DefaultTimeout)
+	_, diags := pt.client.timeouts.Read(ctx, DefaultTimeout)
 	resp.Diagnostics.Append(diags...)
 
 	if resp.Diagnostics.HasError() {
@@ -105,14 +104,11 @@ func (pt *pipelineTemplateDatasource) Read(ctx context.Context, req datasource.R
 
 	if !state.ID.IsNull() {
 		var apiResponse *getNodeResponse
-		err := retry.RetryContext(ctx, timeouts, func() *retry.RetryError {
-			var err error
-			apiResponse, err = getNode(ctx, pt.client.genqlient, state.ID.ValueString())
-			return retryContextError(err)
-		})
+		var err error
+		apiResponse, err = getNode(ctx, pt.client.genqlient, state.ID.ValueString())
 		if err != nil {
 			resp.Diagnostics.AddError(
-				"Unable to get pipeline template",
+				"Unable to get Pipeline Template by ID",
 				fmt.Sprintf("Error getting pipeline template: %s", err.Error()),
 			)
 			return
@@ -130,47 +126,40 @@ func (pt *pipelineTemplateDatasource) Read(ctx context.Context, req datasource.R
 			updatePipelineTemplateDatasourceState(&state, *pipelineTemplateNode)
 		}
 	} else if !state.Name.IsNull() {
+		var r *getPipelineTemplatesResponse
+		var err error
+		cursor := (*string)(nil)
 		matchFound := false
-		err := retry.RetryContext(ctx, timeouts, func() *retry.RetryError {
-			var cursor *string
-			for {
-				r, err := getPipelineTemplates(
-					ctx,
-					pt.client.genqlient,
-					pt.client.organization,
-					cursor)
-				if err != nil {
-					if isRetryableError(err) {
-						return retry.RetryableError(err)
-					}
-					resp.Diagnostics.AddError(
-						"Unable to read pipeline templates",
-						fmt.Sprintf("Unable to read pipeline templates: %s", err.Error()),
-					)
-					return retry.NonRetryableError(err)
-				}
 
-				for _, template := range r.Organization.PipelineTemplates.Edges {
-					if template.Node.Name == state.Name.ValueString() {
-						matchFound = true
-						updatePipelineTemplateDatasourceFromNode(&state, template.Node)
-						break
-					}
-				}
+		for {
+			r, err = getPipelineTemplates(
+				ctx,
+				pt.client.genqlient,
+				pt.client.organization,
+				cursor)
+			if err != nil {
+				resp.Diagnostics.AddError(
+					"Unable to read pipeline templates",
+					fmt.Sprintf("Unable to read pipeline templates: %s", err.Error()),
+				)
+				return
+			}
 
-				// If no match found and at the last page, break
-				if matchFound || !r.Organization.PipelineTemplates.PageInfo.HasNextPage {
+			for _, template := range r.Organization.PipelineTemplates.Edges {
+				if template.Node.Name == state.Name.ValueString() {
+					matchFound = true
+					updatePipelineTemplateDatasourceFromNode(&state, template.Node)
 					break
 				}
+			}
 
-				// Move to next cursor
-				cursor = &r.Organization.PipelineTemplates.PageInfo.EndCursor
+			// If no match found and at the last page, break
+			if matchFound || !r.Organization.PipelineTemplates.PageInfo.HasNextPage {
+				break
 			}
-			return nil
-		})
-		if err != nil {
-			resp.Diagnostics.AddError("Unable to find pipeline template", err.Error())
-			return
+
+			// Move to next cursor
+			cursor = &r.Organization.PipelineTemplates.PageInfo.EndCursor
 		}
 
 		if !matchFound {
@@ -179,6 +168,7 @@ func (pt *pipelineTemplateDatasource) Read(ctx context.Context, req datasource.R
 			return
 		}
 	}
+
 	resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
 }
 
diff --git a/buildkite/data_source_test_suite.go b/buildkite/data_source_test_suite.go
index 3d806d6e..3853f433 100644
--- a/buildkite/data_source_test_suite.go
+++ b/buildkite/data_source_test_suite.go
@@ -48,9 +48,9 @@ func (t *testSuiteDatasource) Read(ctx context.Context, req datasource.ReadReque
 		return
 	}
 
-	suite := testSuiteResponse{}
-	url := fmt.Sprintf("/v2/analytics/organizations/%s/suites/%s", t.client.organization, state.Slug.ValueString())
-	err := t.client.makeRequest(ctx, "GET", url, nil, &suite)
+	var suite testSuiteResponse
+	var url = fmt.Sprintf("/v2/analytics/organizations/%s/suites/%s", t.client.organization, state.Slug.ValueString())
+	var err = t.client.makeRequest(ctx, "GET", url, nil, &suite)
 	if err != nil {
 		resp.Diagnostics.AddError(
 			"Failed to read test suite",
diff --git a/buildkite/provider.go b/buildkite/provider.go
index b28f37da..a649bb2a 100644
--- a/buildkite/provider.go
+++ b/buildkite/provider.go
@@ -21,7 +21,7 @@ const (
 	defaultGraphqlEndpoint = "https://graphql.buildkite.com/v1"
 	defaultRestEndpoint    = "https://api.buildkite.com"
 
-	DefaultTimeout = 30 * time.Second
+	DefaultTimeout = 180 * time.Second
 )
 
 const (
diff --git a/buildkite/resource_cluster_queue.go b/buildkite/resource_cluster_queue.go
index 7d9cc3f7..cbf4ed1b 100644
--- a/buildkite/resource_cluster_queue.go
+++ b/buildkite/resource_cluster_queue.go
@@ -19,7 +19,6 @@ import (
 	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
 	"github.com/hashicorp/terraform-plugin-framework/types"
 	"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
-	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
 )
 
 const (
@@ -232,7 +231,7 @@ func (cq *clusterQueueResource) Create(ctx context.Context, req resource.CreateR
 		return
 	}
 
-	var hosted *HostedAgentsQueueSettingsCreateInput
+	hosted := (*HostedAgentsQueueSettingsCreateInput)(nil)
 	if plan.HostedAgents != nil {
 		hosted = &HostedAgentsQueueSettingsCreateInput{
 			InstanceShape: HostedAgentInstanceShapeName(plan.HostedAgents.InstanceShape.ValueString()),
@@ -253,24 +252,21 @@ func (cq *clusterQueueResource) Create(ctx context.Context, req resource.CreateR
 		}
 	}
 
-	var r *createClusterQueueResponse
-	err := retry.RetryContext(ctx, timeout, func() *retry.RetryError {
-		org, err := cq.client.GetOrganizationID()
-		if err == nil {
-			log.Printf("Creating cluster queue with key %s into cluster %s ...", plan.Key.ValueString(), plan.ClusterId.ValueString())
-
-			r, err = createClusterQueue(ctx,
-				cq.client.genqlient,
-				*org,
-				plan.ClusterId.ValueString(),
-				plan.Key.ValueString(),
-				plan.Description.ValueStringPointer(),
-				hosted,
-			)
-		}
+	org, err := cq.client.GetOrganizationID()
+	if err != nil {
+		resp.Diagnostics.AddError("Unable to get organization ID", fmt.Sprintf("Failed to get organization ID: %s", err.Error()))
+		return
+	}
 
-		return retryContextError(err)
-	})
+	log.Printf("Creating cluster queue with key %s into cluster %s ...", plan.Key.ValueString(), plan.ClusterId.ValueString())
+	r, err := createClusterQueue(ctx,
+		cq.client.genqlient,
+		*org,
+		plan.ClusterId.ValueString(),
+		plan.Key.ValueString(),
+		plan.Description.ValueStringPointer(),
+		hosted,
+	)
 	if err != nil {
 		resp.Diagnostics.AddError(
 			"Unable to create Cluster Queue",
@@ -286,18 +282,19 @@ func (cq *clusterQueueResource) Create(ctx context.Context, req resource.CreateR
 	state.Key = types.StringValue(r.ClusterQueueCreate.ClusterQueue.Key)
 	state.Description = types.StringPointerValue(r.ClusterQueueCreate.ClusterQueue.Description)
 
+	state.DispatchPaused = types.BoolValue(false) // Start with false, update below if needed
+
 	// GraphQL API does not allow Cluster Queue to be created with Dispatch Paused
-	// so Pause Dispatch after creation
+	// so Pause Dispatch after creation if required
 	if plan.DispatchPaused.ValueBool() {
-		err := retry.RetryContext(ctx, timeout, func() *retry.RetryError {
-			log.Printf("Pausing dispatch on cluster queue with key %s", plan.Key.ValueString())
-			return retryContextError(cq.pauseDispatch(ctx, timeout, state, &resp.Diagnostics))
-		})
+		log.Printf("Pausing dispatch on cluster queue with key %s", plan.Key.ValueString())
+		err = cq.pauseDispatch(ctx, timeout, state, &resp.Diagnostics)
 		if err != nil {
+			// Error is added to diagnostics within pauseDispatch
 			return
 		}
+		state.DispatchPaused = types.BoolValue(true)
 	}
-	state.DispatchPaused = plan.DispatchPaused
 
 	if plan.HostedAgents != nil {
 		state.HostedAgents = &hostedAgentResourceModel{
@@ -328,53 +325,51 @@ func (cq *clusterQueueResource) Read(ctx context.Context, req resource.ReadReque
 		return
 	}
 
-	timeout, diags := cq.client.timeouts.Read(ctx, DefaultTimeout)
-	resp.Diagnostics.Append(diags...)
+	// Timeout is not used here
+	_, readDiags := cq.client.timeouts.Read(ctx, DefaultTimeout)
+	resp.Diagnostics.Append(readDiags...)
 
 	if resp.Diagnostics.HasError() {
 		return
 	}
 
-	var matchFound bool
-	err := retry.RetryContext(ctx, timeout, func() *retry.RetryError {
-		var cursor *string
-		for {
-			log.Printf("Getting cluster queues for cluster %s ...", state.ClusterUuid.ValueString())
-			r, err := getClusterQueues(ctx, cq.client.genqlient, cq.client.organization, state.ClusterUuid.ValueString(), cursor)
-			if err != nil {
-				if isRetryableError(err) {
-					return retry.RetryableError(err)
-				}
-				resp.Diagnostics.AddError(
-					"Unable to read Cluster Queues",
-					fmt.Sprintf("Unable to read Cluster Queues: %s", err.Error()),
-				)
-				return retry.NonRetryableError(err)
-			}
+	var r *getClusterQueuesResponse
+	var err error
+	cursor := (*string)(nil)
+	matchFound := false
 
-			// Find the cluster queue from the returned queues to update state
-			for _, edge := range r.Organization.Cluster.Queues.Edges {
-				if edge.Node.Id == state.Id.ValueString() {
-					matchFound = true
-					log.Printf("Found cluster queue with ID %s in cluster %s", edge.Node.Id, state.ClusterUuid.ValueString())
-					// Update ClusterQueueResourceModel with Node values and append
-					updateClusterQueueResource(edge.Node, &state)
-					resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
-					break
-				}
-			}
+	for {
+		log.Printf("Getting cluster queues for cluster %s ...", state.ClusterUuid.ValueString())
+		r, err = getClusterQueues(ctx, cq.client.genqlient, cq.client.organization, state.ClusterUuid.ValueString(), cursor)
+		if err != nil {
+			resp.Diagnostics.AddError(
+				"Unable to read Cluster Queues",
+				fmt.Sprintf("Unable to read Cluster Queues: %s", err.Error()),
+			)
+			return
+		}
 
-			// end here if we found a match or there are no more pages to search
-			if matchFound || !r.Organization.Cluster.Queues.PageInfo.HasNextPage {
+		// Find the cluster queue from the returned queues to update state
+		for _, edge := range r.Organization.Cluster.Queues.Edges {
+			if edge.Node.Id == state.Id.ValueString() {
+				matchFound = true
+				log.Printf("Found cluster queue with ID %s in cluster %s", edge.Node.Id, state.ClusterUuid.ValueString())
+				// Update ClusterQueueResourceModel with Node values and append
+				updateClusterQueueResource(edge.Node, &state)
+				resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
 				break
 			}
-			cursor = &r.Organization.Cluster.Queues.PageInfo.EndCursor
 		}
-		return nil
-	})
+
+		// end here if we found a match or there are no more pages to search
+		if matchFound || !r.Organization.Cluster.Queues.PageInfo.HasNextPage {
+			break
+		}
+		cursor = &r.Organization.Cluster.Queues.PageInfo.EndCursor
+	}
 
 	// Cluster queue could not be found in returned queues and should be removed from state
-	if !matchFound || err != nil {
+	if !matchFound {
 		resp.Diagnostics.AddWarning(
 			"Cluster Queue not found",
 			"Removing Cluster Queue from state...",
@@ -419,7 +414,7 @@ func (cq *clusterQueueResource) Update(ctx context.Context, req resource.UpdateR
 	}
 
 	var r *updateClusterQueueResponse
-	var hosted *HostedAgentsQueueSettingsUpdateInput
+	hosted := (*HostedAgentsQueueSettingsUpdateInput)(nil)
 	if state.HostedAgents != nil {
 		hosted = &HostedAgentsQueueSettingsUpdateInput{
 			InstanceShape: HostedAgentInstanceShapeName(plan.HostedAgents.InstanceShape.ValueString()),
@@ -448,42 +443,44 @@ func (cq *clusterQueueResource) Update(ctx context.Context, req resource.UpdateR
 		}
 	}
 
-	err := retry.RetryContext(ctx, timeout, func() *retry.RetryError {
-		org, err := cq.client.GetOrganizationID()
-		if err == nil {
-			log.Printf("Updating cluster queue %s ...", state.Id.ValueString())
+	org, err := cq.client.GetOrganizationID()
+	if err != nil {
+		resp.Diagnostics.AddError("Unable to get organization ID", fmt.Sprintf("Failed to get organization ID: %s", err.Error()))
+		return
+	}
 
-			// Extract the planned and state values for DispatchPaused attribute
-			// to compare if the Queue should be paused or resumed
-			// if neither do nothing
-			planDispatchPaused = plan.DispatchPaused.ValueBool()
-			stateDispatchPaused = state.DispatchPaused.ValueBool()
+	log.Printf("Updating cluster queue %s ...", state.Id.ValueString())
 
-			// Check the planned value against the current state value
-			// Planned to be true (changing from false to true)
-			if planDispatchPaused && !stateDispatchPaused {
-				if err := cq.pauseDispatch(ctx, timeout, state, &resp.Diagnostics); err != nil {
-					return retryContextError(err)
-				}
-			}
+	// Extract the planned and state values for DispatchPaused attribute
+	// to compare if the Queue should be paused or resumed
+	// if neither do nothing
+	planDispatchPaused = plan.DispatchPaused.ValueBool()
+	stateDispatchPaused = state.DispatchPaused.ValueBool()
 
-			// Planned to be false (changing from true to false)
-			if !planDispatchPaused && stateDispatchPaused {
-				if err := cq.resumeDispatch(ctx, timeout, state, &resp.Diagnostics); err != nil {
-					return retryContextError(err)
-				}
-			}
+	// Check the planned value against the current state value
+	// Planned to be true (changing from false to true)
+	if planDispatchPaused && !stateDispatchPaused {
+		if err := cq.pauseDispatch(ctx, timeout, state, &resp.Diagnostics); err != nil {
+			// Error added to diagnostics within pauseDispatch
+			return
+		}
+	}
 
-			r, err = updateClusterQueue(ctx,
-				cq.client.genqlient,
-				*org,
-				state.Id.ValueString(),
-				plan.Description.ValueStringPointer(),
-				hosted,
-			)
+	// Planned to be false (changing from true to false)
+	if !planDispatchPaused && stateDispatchPaused {
+		if err := cq.resumeDispatch(ctx, timeout, state, &resp.Diagnostics); err != nil {
+			// Error added to diagnostics within resumeDispatch
+			return
 		}
-		return retryContextError(err)
-	})
+	}
+
+	r, err = updateClusterQueue(ctx,
+		cq.client.genqlient,
+		*org,
+		state.Id.ValueString(),
+		plan.Description.ValueStringPointer(),
+		hosted,
+	)
 	if err != nil {
 		resp.Diagnostics.AddError(
 			"Unable to update Cluster Queue",
@@ -523,29 +520,28 @@ func (cq *clusterQueueResource) Delete(ctx context.Context, req resource.DeleteR
 		return
 	}
 
-	timeout, diags := cq.client.timeouts.Delete(ctx, DefaultTimeout)
-	resp.Diagnostics.Append(diags...)
+	_, deleteDiags := cq.client.timeouts.Delete(ctx, DefaultTimeout)
+	resp.Diagnostics.Append(deleteDiags...)
 
 	if resp.Diagnostics.HasError() {
 		return
 	}
 
-	err := retry.RetryContext(ctx, timeout, func() *retry.RetryError {
-		org, err := cq.client.GetOrganizationID()
-		if err == nil {
-			log.Printf("Deleting cluster queue %s ...", plan.Id.ValueString())
-			_, err = deleteClusterQueue(ctx,
-				cq.client.genqlient,
-				*org,
-				plan.Id.ValueString(),
-			)
-		}
+	org, err := cq.client.GetOrganizationID()
+	if err != nil {
+		resp.Diagnostics.AddError("Unable to get organization ID", fmt.Sprintf("Failed to get organization ID: %s", err.Error()))
+		return
+	}
 
-		return retryContextError(err)
-	})
+	log.Printf("Deleting cluster queue %s ...", plan.Id.ValueString())
+	_, err = deleteClusterQueue(ctx,
+		cq.client.genqlient,
+		*org,
+		plan.Id.ValueString(),
+	)
 	if err != nil {
 		resp.Diagnostics.AddError(
-			"Unable to create Cluster Queue",
+			"Unable to delete Cluster Queue",
 			fmt.Sprintf("Unable to delete Cluster Queue: %s", err.Error()),
 		)
 		return
@@ -664,14 +660,11 @@ func updateClusterQueueResource(clusterQueueNode getClusterQueuesOrganizationClu
 }
 
 func (cq *clusterQueueResource) pauseDispatch(ctx context.Context, timeout time.Duration, state clusterQueueResourceModel, diag *diag.Diagnostics) error {
-	err := retry.RetryContext(ctx, timeout, func() *retry.RetryError {
-		log.Printf("Pausing dispatch for cluster queue %s", state.Key)
-		_, err := pauseDispatchClusterQueue(ctx, cq.client.genqlient, state.Id.ValueString())
-		return retryContextError(err)
-	})
+	log.Printf("Pausing dispatch for cluster queue %s", state.Key.ValueString())
+	_, err := pauseDispatchClusterQueue(ctx, cq.client.genqlient, state.Id.ValueString())
 	if err != nil {
 		diag.AddError(
-			"Unable to pause cluster queue dispatch",
+			"Unable to pause Cluster Queue dispatch",
 			fmt.Sprintf("Unable to pause dispatch for cluster queue: %s", err.Error()),
 		)
 	}
@@ -680,14 +673,11 @@ func (cq *clusterQueueResource) pauseDispatch(ctx context.Context, timeout time.
 }
 
 func (cq *clusterQueueResource) resumeDispatch(ctx context.Context, timeout time.Duration, state clusterQueueResourceModel, diag *diag.Diagnostics) error {
-	err := retry.RetryContext(ctx, timeout, func() *retry.RetryError {
-		log.Printf("Resuming dispatch for cluster queue %s", state.Key)
-		_, err := resumeDispatchClusterQueue(ctx, cq.client.genqlient, state.Id.ValueString())
-		return retryContextError(err)
-	})
+	log.Printf("Resuming dispatch for cluster queue %s", state.Key.ValueString())
+	_, err := resumeDispatchClusterQueue(ctx, cq.client.genqlient, state.Id.ValueString())
 	if err != nil {
 		diag.AddError(
-			"Unable to resume cluster queue dispatch",
+			"Unable to resume Cluster Queue dispatch",
 			fmt.Sprintf("Unable to resume dispatch for cluster queue: %s", err.Error()),
 		)
 	}
diff --git a/buildkite/resource_pipeline.go b/buildkite/resource_pipeline.go
index a63d39d8..c8fc8ed9 100644
--- a/buildkite/resource_pipeline.go
+++ b/buildkite/resource_pipeline.go
@@ -1055,10 +1055,10 @@ type PipelineExtraSettings struct {
 }
 
 func getPipelineExtraInfo(ctx context.Context, client *Client, slug string, timeouts time.Duration) (*PipelineExtraInfo, error) {
-	pipelineExtraInfo := PipelineExtraInfo{}
+	var pipelineExtraInfo PipelineExtraInfo
 
-	err := retry.RetryContext(ctx, timeouts, func() *retry.RetryError {
-		err := client.makeRequest(ctx, "GET", fmt.Sprintf("/v2/organizations/%s/pipelines/%s", client.organization, slug), nil, &pipelineExtraInfo)
+	var err = retry.RetryContext(ctx, timeouts, func() *retry.RetryError {
+		var err = client.makeRequest(ctx, "GET", fmt.Sprintf("/v2/organizations/%s/pipelines/%s", client.organization, slug), nil, &pipelineExtraInfo)
 		return retryContextError(err)
 	})
 	if err != nil {
@@ -1073,11 +1073,11 @@ func updatePipelineSlug(ctx context.Context, slug string, updatedSlug string, cl
 		Slug: updatedSlug,
 	}
 
-	pipelineExtraInfo := PipelineExtraInfo{}
+	var pipelineExtraInfo PipelineExtraInfo
 
 	if len(updatedSlug) > 0 {
-		err := retry.RetryContext(ctx, timeouts, func() *retry.RetryError {
-			err := client.makeRequest(ctx, "PATCH", fmt.Sprintf("/v2/organizations/%s/pipelines/%s", client.organization, slug), payload, &pipelineExtraInfo)
+		var err = retry.RetryContext(ctx, timeouts, func() *retry.RetryError {
+			var err = client.makeRequest(ctx, "PATCH", fmt.Sprintf("/v2/organizations/%s/pipelines/%s", client.organization, slug), payload, &pipelineExtraInfo)
 			return retryContextError(err)
 		})
 		if err != nil {
@@ -1112,9 +1112,9 @@ func updatePipelineExtraInfo(ctx context.Context, slug string, settings *provide
 		},
 	}
 
-	pipelineExtraInfo := PipelineExtraInfo{}
-	err := retry.RetryContext(ctx, timeouts, func() *retry.RetryError {
-		err := client.makeRequest(ctx, "PATCH", fmt.Sprintf("/v2/organizations/%s/pipelines/%s", client.organization, slug), payload, &pipelineExtraInfo)
+	var pipelineExtraInfo PipelineExtraInfo
+	var err = retry.RetryContext(ctx, timeouts, func() *retry.RetryError {
+		var err = client.makeRequest(ctx, "PATCH", fmt.Sprintf("/v2/organizations/%s/pipelines/%s", client.organization, slug), payload, &pipelineExtraInfo)
 		return retryContextError(err)
 	})
 	if err != nil {
diff --git a/buildkite/resource_pipeline_team_test.go b/buildkite/resource_pipeline_team_test.go
index dabf22e8..01f6b977 100644
--- a/buildkite/resource_pipeline_team_test.go
+++ b/buildkite/resource_pipeline_team_test.go
@@ -168,7 +168,9 @@ func testAccCheckPipelineTeamExists(resourceName string, tp *pipelineTeamResourc
 			return fmt.Errorf("No ID is set in state")
 		}
 
-		apiResponse, err := getNode(context.Background(), genqlientGraphql, resourceState.Primary.ID)
+		var apiResponse *getNodeResponse
+		var err error
+		apiResponse, err = getNode(context.Background(), genqlientGraphql, resourceState.Primary.ID)
 		if err != nil {
 			return fmt.Errorf("Error fetching team pipeline from graphql API: %v", err)
 		}
@@ -191,7 +193,9 @@ func testCheckPipelineTeamResourceRemoved(s *terraform.State) error {
 			continue
 		}
 
-		apiResponse, err := getNode(context.Background(), genqlientGraphql, rs.Primary.ID)
+		var apiResponse *getNodeResponse
+		var err error
+		apiResponse, err = getNode(context.Background(), genqlientGraphql, rs.Primary.ID)
 		if err != nil {
 			return fmt.Errorf("Error fetching team pipeline from graphql API: %v", err)
 		}
diff --git a/buildkite/resource_registry.go b/buildkite/resource_registry.go
index 64ef6d8f..f095f184 100644
--- a/buildkite/resource_registry.go
+++ b/buildkite/resource_registry.go
@@ -133,7 +133,7 @@ func (p *registryResource) Create(ctx context.Context, req resource.CreateReques
 	}
 
 	err := retry.RetryContext(ctx, timeout, func() *retry.RetryError {
-		url := fmt.Sprintf("%s/v2/packages/organizations/%s/registries", p.client.restURL, p.client.organization)
+		var url = fmt.Sprintf("%s/v2/packages/organizations/%s/registries", p.client.restURL, p.client.organization)
 
 		reqBody := map[string]interface{}{
 			"name":      state.Name.ValueString(),
@@ -283,7 +283,7 @@ func (p *registryResource) Read(ctx context.Context, req resource.ReadRequest, r
 		if state.Slug.IsNull() || state.Slug.ValueString() == "" {
 			// If no slug is found, we need to fetch all registries and find by name
 			// This handles the case during import or when slug isn't in state
-			url := fmt.Sprintf("%s/v2/packages/organizations/%s/registries", p.client.restURL, p.client.organization)
+			var url = fmt.Sprintf("%s/v2/packages/organizations/%s/registries", p.client.restURL, p.client.organization)
 
 			req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
 			if err != nil {
@@ -378,7 +378,7 @@ func (p *registryResource) Read(ctx context.Context, req resource.ReadRequest, r
 		}
 
 		// We have a UUID, use it to directly fetch the registry
-		url := fmt.Sprintf("%s/v2/packages/organizations/%s/registries/%s", p.client.restURL, p.client.organization, state.Slug.ValueString())
+		var url = fmt.Sprintf("%s/v2/packages/organizations/%s/registries/%s", p.client.restURL, p.client.organization, state.Slug.ValueString())
 
 		req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
 		if err != nil {
@@ -567,7 +567,7 @@ func (p *registryResource) Update(ctx context.Context, req resource.UpdateReques
 		readTimeout, _ := p.client.timeouts.Read(ctx, DefaultTimeout)
 
 		err := retry.RetryContext(ctx, readTimeout, func() *retry.RetryError {
-			url := fmt.Sprintf("%s/v2/packages/organizations/%s/registries", p.client.restURL, p.client.organization)
+			var url = fmt.Sprintf("%s/v2/packages/organizations/%s/registries", p.client.restURL, p.client.organization)
 
 			req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
 			if err != nil {
@@ -636,7 +636,7 @@ func (p *registryResource) Update(ctx context.Context, req resource.UpdateReques
 			return retry.NonRetryableError(fmt.Errorf("no valid ID or UUID available for registry lookup"))
 		}
 
-		url := fmt.Sprintf("%s/v2/packages/organizations/%s/registries/%s", p.client.restURL, p.client.organization, lookupID)
+		var url = fmt.Sprintf("%s/v2/packages/organizations/%s/registries/%s", p.client.restURL, p.client.organization, lookupID)
 
 		reqBody := map[string]interface{}{
 			"name":      plan.Name.ValueString(),
@@ -858,7 +858,7 @@ func (p *registryResource) Delete(ctx context.Context, req resource.DeleteReques
 		readTimeout, _ := p.client.timeouts.Read(ctx, DefaultTimeout)
 
 		_ = retry.RetryContext(ctx, readTimeout, func() *retry.RetryError {
-			url := fmt.Sprintf("%s/v2/packages/organizations/%s/registries", p.client.restURL, p.client.organization)
+			var url = fmt.Sprintf("%s/v2/packages/organizations/%s/registries", p.client.restURL, p.client.organization)
 
 			req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
 			if err != nil {
@@ -922,7 +922,7 @@ func (p *registryResource) Delete(ctx context.Context, req resource.DeleteReques
 			return nil
 		}
 
-		url := fmt.Sprintf("%s/v2/packages/organizations/%s/registries/%s", p.client.restURL, p.client.organization, lookupID)
+		var url = fmt.Sprintf("%s/v2/packages/organizations/%s/registries/%s", p.client.restURL, p.client.organization, lookupID)
 
 		// Create the HTTP request
 		req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil)
diff --git a/buildkite/resource_registry_test.go b/buildkite/resource_registry_test.go
index 508d021c..720b1dff 100644
--- a/buildkite/resource_registry_test.go
+++ b/buildkite/resource_registry_test.go
@@ -136,7 +136,7 @@ func testAccCheckRegistryDestroy(s *terraform.State) error {
 		slug := rs.Primary.Attributes["slug"]
 
 		// Make API call to check if the registry still exists
-		url := fmt.Sprintf("%s/v2/packages/organizations/%s/registries/%s", baseURL, orgSlug, slug)
+		var url = fmt.Sprintf("%s/v2/packages/organizations/%s/registries/%s", baseURL, orgSlug, slug)
 
 		req, err := http.NewRequest(http.MethodGet, url, nil)
 		if err != nil {
diff --git a/buildkite/resource_test_suite.go b/buildkite/resource_test_suite.go
index 7a8bf90d..88bf3779 100644
--- a/buildkite/resource_test_suite.go
+++ b/buildkite/resource_test_suite.go
@@ -52,7 +52,7 @@ func (ts *testSuiteResource) Configure(ctx context.Context, req resource.Configu
 func (ts *testSuiteResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
 	var plan, state testSuiteModel
 	var response testSuiteResponse
-	payload := map[string]interface{}{}
+	var payload = map[string]interface{}{}
 	var teamOwnerUuid string
 
 	diags := req.Plan.Get(ctx, &plan)
@@ -99,7 +99,7 @@ func (ts *testSuiteResource) Create(ctx context.Context, req resource.CreateRequ
 	payload["team_ids"] = []string{teamOwnerUuid}
 
 	// Construct URL to call to the REST API
-	url := fmt.Sprintf("/v2/analytics/organizations/%s/suites", ts.client.organization)
+	var url = fmt.Sprintf("/v2/analytics/organizations/%s/suites", ts.client.organization)
 
 	// Use the Create timeout for test suite creation
 	timeout, diags = ts.client.timeouts.Create(ctx, DefaultTimeout)
@@ -144,9 +144,9 @@ func (ts *testSuiteResource) Delete(ctx context.Context, req resource.DeleteRequ
 	resp.Diagnostics.Append(diags...)
 
 	// Construct URL to call to the REST API
-	url := fmt.Sprintf("/v2/analytics/organizations/%s/suites/%s", ts.client.organization, state.Slug.ValueString())
-	err := retry.RetryContext(ctx, timeout, func() *retry.RetryError {
-		err := ts.client.makeRequest(ctx, "DELETE", url, nil, nil)
+	var url = fmt.Sprintf("/v2/analytics/organizations/%s/suites/%s", ts.client.organization, state.Slug.ValueString())
+	var err = retry.RetryContext(ctx, timeout, func() *retry.RetryError {
+		var err = ts.client.makeRequest(ctx, "DELETE", url, nil, nil)
 
 		return retryContextError(err)
 	})
@@ -286,7 +286,7 @@ func (ts *testSuiteResource) Schema(ctx context.Context, req resource.SchemaRequ
 func (ts *testSuiteResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
 	var plan, state testSuiteModel
 	var response testSuiteResponse
-	payload := map[string]interface{}{}
+	var payload = map[string]interface{}{}
 
 	diagsPlan := req.Plan.Get(ctx, &plan)
 	diagsState := req.State.Get(ctx, &state)
@@ -309,9 +309,9 @@ func (ts *testSuiteResource) Update(ctx context.Context, req resource.UpdateRequ
 	payload["default_branch"] = plan.DefaultBranch.ValueString()
 
 	// Construct URL to call to the REST API
-	url := fmt.Sprintf("/v2/analytics/organizations/%s/suites/%s", ts.client.organization, state.Slug.ValueString())
-	updateErr := retry.RetryContext(ctx, timeout, func() *retry.RetryError {
-		err := ts.client.makeRequest(ctx, "PATCH", url, payload, &response)
+	var url = fmt.Sprintf("/v2/analytics/organizations/%s/suites/%s", ts.client.organization, state.Slug.ValueString())
+	var updateErr = retry.RetryContext(ctx, timeout, func() *retry.RetryError {
+		var err = ts.client.makeRequest(ctx, "PATCH", url, payload, &response)
 
 		return retryContextError(err)
 	})
diff --git a/buildkite/util.go b/buildkite/util.go
index 9478a5ec..8b0dfc83 100644
--- a/buildkite/util.go
+++ b/buildkite/util.go
@@ -80,11 +80,12 @@ func createCidrSliceFromList(cidrList types.List) []string {
 	return cidrs
 }
 
+// retryContextError wraps an error for use with hashicorp/terraform-plugin-sdk/v2/helper/retry.
+// Since the underlying http client now handles retries, we always treat errors as non-retryable
+// at this layer to avoid duplicate retries.
 func retryContextError(err error) *retry.RetryError {
 	if err != nil {
-		if isRetryableError(err) {
-			return retry.RetryableError(err)
-		}
+		// Always return NonRetryableError as the http client handles retries
 		return retry.NonRetryableError(err)
 	}
 	return nil
diff --git a/go.mod b/go.mod
index e96e2275..85f874ce 100644
--- a/go.mod
+++ b/go.mod
@@ -6,6 +6,7 @@ require (
 	github.com/Khan/genqlient v0.7.0
 	github.com/buildkite/go-pipeline v0.13.1
 	github.com/buildkite/interpolate v0.1.3
+	github.com/hashicorp/go-retryablehttp v0.7.7
 	github.com/hashicorp/terraform-plugin-framework v1.14.0
 	github.com/hashicorp/terraform-plugin-framework-validators v0.17.0
 	github.com/hashicorp/terraform-plugin-go v0.26.0
@@ -21,7 +22,6 @@ require (
 	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
 	github.com/goccy/go-json v0.10.3 // indirect
 	github.com/gowebpki/jcs v1.0.1 // indirect
-	github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
 	github.com/kr/pretty v0.3.0 // indirect
 	github.com/lestrrat-go/blackmagic v1.0.2 // indirect
 	github.com/lestrrat-go/httpcc v1.0.1 // indirect
@@ -60,7 +60,7 @@ require (
 	github.com/hashicorp/terraform-exec v0.22.0 // indirect
 	github.com/hashicorp/terraform-json v0.24.0 // indirect
 	github.com/hashicorp/terraform-plugin-framework-timeouts v0.5.0
-	github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
+	github.com/hashicorp/terraform-plugin-log v0.9.0
 	github.com/hashicorp/terraform-registry-address v0.2.4 // indirect
 	github.com/hashicorp/terraform-svchost v0.1.1 // indirect
 	github.com/hashicorp/yamux v0.1.1 // indirect
diff --git a/lefthook.yml b/lefthook.yml
new file mode 100644
index 00000000..de4874a3
--- /dev/null
+++ b/lefthook.yml
@@ -0,0 +1,8 @@
+pre-commit:
+  commands:
+    gofmt:
+      glob: "*.go"
+      run: gofmt -s -w buildkite
+    gomodtidy:
+      glob: "*.go"
+      run: go mod tidy


Reviews:
user: login: PriyaSudip
body: Still reviewing but tests seem to have failed
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-08T00:08:48Z
user: login: PriyaSudip
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-09T03:32:20Z
user: login: PriyaSudip
body: Apart from the above, rest all looks good to me.
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-09T05:57:12Z
user: login: scadu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T07:36:48Z
user: login: petetomasik
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T15:25:03Z

Review comments:
diff_hunk: @@ -53,33 +56,138 @@ func (client Client) GetOrganizationID() (*string, error) {
 	return client.organizationId, nil
 }
 
-// NewClient creates a client to use for interacting with the Buildkite API
+// NewClient creates a client for interacting with the Buildkite API.
+//
+// https://buildkite.com/docs/apis/rest-api/limits
+//
+// For REST API calls:
+//  1. Uses hashicorp/go-retryablehttp to provide automatic retries with smart backoff
+//  2. Maximum of 10 retry attempts for requests that fail with retryable errors
+//  3. Rate limiting strategy:
+//     - Checks RateLimit-Reset header to determine when the rate limit will be reset
+//     - Waits until the reset time plus a small buffer before retrying
+//     - Falls back to Retry-After header if reset time isn't available
+//  4. Also retries server errors (HTTP 500-599) with linear jitter backoff
+//  5. All retryable requests have a minimum wait of 15 seconds and maximum of 180 seconds
+
 func NewClient(config *clientConfig) *Client {
-	// Setup a HTTP Client that can be used by all REST and graphql API calls,
-	// with suitable headers for authentication and user agent identification
-	rt := http.DefaultTransport
+	// Create standard HTTP client for GraphQL
+	standardHttpClient := &http.Client{}
+
+	// Set timeout if configured
+	readTimeout, diags := config.timeouts.Read(context.Background(), DefaultTimeout)
+	if !diags.HasError() && readTimeout > 0 {
+		standardHttpClient.Timeout = readTimeout
+	}
+
+	// Set up authentication and user agent headers
 	header := make(http.Header)
 	header.Set("Authorization", "Bearer "+config.apiToken)
 	header.Set("User-Agent", config.userAgent)
-	rt = newHeaderRoundTripper(rt, header)
+	standardHttpClient.Transport = newHeaderRoundTripper(standardHttpClient.Transport, header)
+
+	// Create retryable client with rate limit handling for REST API calls
+	retryClient := retryablehttp.NewClient()
 
-	httpClient := &http.Client{
-		Transport: rt,
+	retryClient.RetryMax = 10
+	retryClient.RetryWaitMin = 15 * time.Second
+	retryClient.RetryWaitMax = 180 * time.Second
+	retryClient.Logger = nil
+
+	if !diags.HasError() && readTimeout > 0 {
+		retryClient.HTTPClient.Timeout = readTimeout
 	}
 
-	graphqlClient := graphql.NewClient(config.graphqlURL, httpClient)
+	// Use LinearJitterBackoff with RateLimit-Reset header support for better distribution of requests
+	retryClient.Backoff = func(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
+		if resp != nil && resp.StatusCode == http.StatusTooManyRequests {
+			// Try to use RateLimit-Reset header first
+			if resetHeader := resp.Header.Get("RateLimit-Reset"); resetHeader != "" {
+				if resetTime, err := strconv.ParseInt(resetHeader, 10, 64); err == nil {
+					resetAt := time.Unix(resetTime, 0)
+					// Add a 2-second buffer to ensure we're past the reset time
+					waitTime := time.Until(resetAt) + (2 * time.Second)
+					tflog.Debug(context.Background(), fmt.Sprintf("Rate limit hit, reset at: %v (waiting: %v)", resetAt, waitTime))
+
+					// Return the wait time within min-max bounds
+					if waitTime < min {
+						return min
+					}
+					if waitTime > max {
+						return max
+					}
+					return waitTime
+				}
+			}
+
+			// Fall back to Retry-After header if available
+			if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
+				if seconds, err := strconv.ParseInt(retryAfter, 10, 64); err == nil {
+					waitTime := time.Duration(seconds) * time.Second
path: buildkite/client.go
user: login: PriyaSudip
body: Should we return waitTime here?
created_at: 2025-04-09T03:29:53Z
author_association: NONE
diff_hunk: @@ -53,33 +56,138 @@ func (client Client) GetOrganizationID() (*string, error) {
 	return client.organizationId, nil
 }
 
-// NewClient creates a client to use for interacting with the Buildkite API
+// NewClient creates a client for interacting with the Buildkite API.
+//
+// https://buildkite.com/docs/apis/rest-api/limits
+//
+// For REST API calls:
+//  1. Uses hashicorp/go-retryablehttp to provide automatic retries with smart backoff
+//  2. Maximum of 10 retry attempts for requests that fail with retryable errors
+//  3. Rate limiting strategy:
+//     - Checks RateLimit-Reset header to determine when the rate limit will be reset
+//     - Waits until the reset time plus a small buffer before retrying
+//     - Falls back to Retry-After header if reset time isn't available
+//  4. Also retries server errors (HTTP 500-599) with linear jitter backoff
+//  5. All retryable requests have a minimum wait of 15 seconds and maximum of 180 seconds
+
 func NewClient(config *clientConfig) *Client {
-	// Setup a HTTP Client that can be used by all REST and graphql API calls,
-	// with suitable headers for authentication and user agent identification
-	rt := http.DefaultTransport
+	// Create standard HTTP client for GraphQL
+	standardHttpClient := &http.Client{}
+
+	// Set timeout if configured
+	readTimeout, diags := config.timeouts.Read(context.Background(), DefaultTimeout)
+	if !diags.HasError() && readTimeout > 0 {
+		standardHttpClient.Timeout = readTimeout
+	}
+
+	// Set up authentication and user agent headers
 	header := make(http.Header)
 	header.Set("Authorization", "Bearer "+config.apiToken)
 	header.Set("User-Agent", config.userAgent)
-	rt = newHeaderRoundTripper(rt, header)
+	standardHttpClient.Transport = newHeaderRoundTripper(standardHttpClient.Transport, header)
+
+	// Create retryable client with rate limit handling for REST API calls
+	retryClient := retryablehttp.NewClient()
 
-	httpClient := &http.Client{
-		Transport: rt,
+	retryClient.RetryMax = 10
+	retryClient.RetryWaitMin = 15 * time.Second
+	retryClient.RetryWaitMax = 180 * time.Second
+	retryClient.Logger = nil
+
+	if !diags.HasError() && readTimeout > 0 {
+		retryClient.HTTPClient.Timeout = readTimeout
 	}
 
-	graphqlClient := graphql.NewClient(config.graphqlURL, httpClient)
+	// Use LinearJitterBackoff with RateLimit-Reset header support for better distribution of requests
+	retryClient.Backoff = func(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
+		if resp != nil && resp.StatusCode == http.StatusTooManyRequests {
+			// Try to use RateLimit-Reset header first
+			if resetHeader := resp.Header.Get("RateLimit-Reset"); resetHeader != "" {
+				if resetTime, err := strconv.ParseInt(resetHeader, 10, 64); err == nil {
+					resetAt := time.Unix(resetTime, 0)
+					// Add a 2-second buffer to ensure we're past the reset time
+					waitTime := time.Until(resetAt) + (2 * time.Second)
+					tflog.Debug(context.Background(), fmt.Sprintf("Rate limit hit, reset at: %v (waiting: %v)", resetAt, waitTime))
+
+					// Return the wait time within min-max bounds
+					if waitTime < min {
+						return min
+					}
+					if waitTime > max {
+						return max
+					}
+					return waitTime
+				}
+			}
+
+			// Fall back to Retry-After header if available
+			if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
+				if seconds, err := strconv.ParseInt(retryAfter, 10, 64); err == nil {
+					waitTime := time.Duration(seconds) * time.Second
path: buildkite/client.go
user: login: scadu
body: Yes, we should! Fixed now.
created_at: 2025-04-09T07:36:48Z
author_association: CONTRIBUTOR

PR comments:
user: login: scadu
created_at: 2025-04-08T06:45:25Z
body: > Still reviewing but tests seem to have failed

@PriyaSudip ah yes, it went rouge after merging changes from main. I’ll work on it today, and remove draft status once tests are cleared. 
author_association: CONTRIBUTOR
user: login: petetomasik
created_at: 2025-04-08T19:38:08Z
body: Small nit on alternating between short assignment statements and var declarations with implicit types with some of the API requests. Other than that, looks good! 👍 
author_association: CONTRIBUTOR
user: login: scadu
created_at: 2025-04-09T07:58:03Z
body: > Small nit on alternating between short assignment statements and var declarations with implicit types with some of the API requests. Other than that, looks good! 👍

@petetomasik aligned styling where possible (to not cause any scoping issues, e.g. reusing values of `ctx` .etc.)
author_association: CONTRIBUTOR