gastownhall/beads · error
idempotency check failed: %w
Error message
idempotency check failed: %w
What it means
Before creating an issue, CreateIssueIdempotent calls FindIssueByDescriptionContains to detect an already-created issue with the same dedup marker. If that search itself fails (transport error, API error, GraphQL error), the idempotency guarantee cannot be established, so the library aborts with this wrapped error instead of risking a duplicate. The underlying cause is always wrapped and available via errors.As/Unwrap.
Source
Thrown at internal/linear/client.go:956
// creating, queries Linear to see if an issue with that marker already exists.
// If a match is found (e.g., from a prior interrupted sync), the existing
// issue is returned without creating a duplicate.
//
// The create is performed as a single attempt (no internal retry) to avoid the
// following race: if issueCreate reaches Linear but the HTTP response is lost
// (network timeout, connection drop), a blind retry would create a second issue
// with the same marker. Instead, after any create failure, this function
// re-searches for the marker so that the caller can safely retry the entire
// CreateIssueIdempotent call and get a consistent result.
//
// Note: concurrent creates from multiple sources (e.g., two sync processes
// running simultaneously) cannot be made fully atomic without server-side
// uniqueness enforcement, which Linear does not provide. The dedup window is
// bounded by Linear's search-index propagation delay.
func (c *Client) CreateIssueIdempotent(ctx context.Context, title, description string, priority int, stateID string, labelIDs []string, marker string) (*Issue, bool, error) {
existing, err := c.FindIssueByDescriptionContains(ctx, marker)
if err != nil {
return nil, false, fmt.Errorf("idempotency check failed: %w", err)
}
if existing != nil {
return existing, true, nil
}
description = AppendIdempotencyMarker(description, marker)
issue, err := c.createIssueSingleAttempt(ctx, title, description, priority, stateID, labelIDs)
if err != nil {
// The mutation may have reached Linear despite the error. Re-check for
// the marker so callers retrying CreateIssueIdempotent get a consistent
// result rather than creating a duplicate.
if found, searchErr := c.FindIssueByDescriptionContains(ctx, marker); searchErr == nil && found != nil {
return found, true, nil
}
return nil, false, err
}
return issue, false, nil
}View on GitHub (pinned to 71377f2769)
Solutions
- Inspect the wrapped cause — it is one of the search-path errors (request failed / API error / GraphQL errors)
- Retry the whole CreateIssueIdempotent call with backoff; the check-then-create is safe to rerun
- Fix auth/rate-limit root causes (valid token, throttling) since the search amplifies API usage
- Accept the documented dedup window: don't treat marker-based dedup as strictly atomic under concurrency
Example fix
// before
issue, existed, err := client.CreateIssueIdempotent(ctx, ...) // transient search failure = hard abort
// after
var issue *linear.Issue
var existed bool
err := retry.OnError(3, backoff, func() error {
var e error
issue, existed, e = client.CreateIssueIdempotent(ctx, title, desc, prio, stateID, labels, marker)
return e
}) Defensive patterns
Strategy: retry
Validate before calling
// validate search preconditions before idempotent create
if marker == "" {
return errors.New("idempotency marker must be non-empty")
}
if err := ctx.Err(); err != nil {
return err
} Try / catch
var issue *linear.Issue
err := retry.Do(3, backoff, func() error {
var e error
issue, _, e = client.CreateIssueIdempotent(ctx, title, desc, prio, stateID, labels, marker)
return e
})
if err != nil && strings.Contains(err.Error(), "idempotency check failed") {
// inspect wrapped cause; do not blind-create without the marker search
} Prevention
- Always retry the full idempotent create rather than falling back to plain create
- Monitor rate limits: each create includes a search call
- Never bypass the marker search manually — duplicates are the risk it guards
- Accept the documented dedup window under high concurrency
When it happens
Trigger: Network failure during the search query; HTTP auth/rate-limit error from Linear during search; GraphQL error in the search query; search index propagation delay causing inconsistent results rather than an error (bounded window, per doc comment).
Common situations: Concurrent workers racing during a Linear outage; expired token surfacing first in the idempotency pre-check; hitting rate limits because every create now does an extra search call.
Related errors
- failed to search issues by description: %w
- batch create failed and recovery search also failed: %w (bat
- batch create failed; %d of %d issues unconfirmed (batch erro
- failed to list projects: %w
- failed to remove backup: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/23107df6c0b18167.
Report an issue: GitHub.