crowdsecurity/crowdsec · error

attach decisions to alert %d: %w

Error message

attach decisions to alert %d: %w

What it means

After the alerts are bulk-inserted, saveAlerts attaches each alert's pre-created decision rows via Alert.Update().AddDecisions(), batched and retried on SQLite busy. This error wraps any failure of that update: the alert row exists (a.ID is printed) but the decision-linking write failed, causing the whole transaction to roll back.

Source

Thrown at pkg/database/alerts.go:621

		return nil, fmt.Errorf("bulk creating alert: %w: %w", err, BulkError)
	}

	ret := make([]string, len(alertsCreateBulk))
	for i, a := range alertsCreateBulk {
		ret[i] = strconv.Itoa(a.ID)

		d := batch[i].decisions
		if len(d) == 0 {
			continue
		}

		if err := slicetools.Batch(ctx, d, c.decisionBulkSize, func(ctx context.Context, d2 []*ent.Decision) error {
			return retryOnBusy(func() error {
				_, err := client.Alert.Update().Where(alert.IDEQ(a.ID)).AddDecisions(d2...).Save(ctx)
				return err
			})
		}); err != nil {
			return nil, fmt.Errorf("attach decisions to alert %d: %w", a.ID, err)
		}
	}

	return ret, nil
}

type alertCreatePlan struct {
	builder   *ent.AlertCreate
	decisions []*ent.Decision
}

func (c *Client) createAlertBatch(ctx context.Context, machineID string, owner *ent.Machine, alerts []*models.Alert) ([]string, error) {
	tx, err := c.Ent.Tx(ctx)
	if err != nil {
		return nil, fmt.Errorf("creating alert transaction: %w: %w", err, BulkError)
	}

	txEnt := tx.Client()

View on GitHub (pinned to 909b515798)

Solutions

  1. Check the wrapped error: if it is 'exceeded N busy retries', reduce concurrent writers on the SQLite DB or switch to MySQL/Postgres for the crowdsec database
  2. Retry the whole CreateAlert call — the transaction rolled back, so alerts were not persisted and it is safe to resubmit
  3. Increase throughput headroom: enable WAL mode for SQLite, or ensure the DB file is on local disk, not a network mount
  4. If it is a constraint error on decisions, inspect the decision payloads for duplicate IDs or invalid references before submitting
  5. Verify DB connectivity and timeouts in the database config if on a network backend

Example fix

// before
ids, err := client.CreateAlert(ctx, machineID, alerts)
if err != nil {
	return fmt.Errorf("create failed: %w", err)
}

// after
ids, err := client.CreateAlert(ctx, machineID, alerts)
if err != nil {
	if strings.Contains(err.Error(), "busy retries") {
		time.Sleep(2 * time.Second)
		ids, err = client.CreateAlert(ctx, machineID, alerts) // tx rolled back: safe retry
	}
	if err != nil {
		return fmt.Errorf("create failed: %w", err)
	}
}
Defensive patterns

Strategy: retry

Validate before calling

// bound the work: cap decisions per alert before submission
if len(alert.Decisions) > maxDecisionsPerAlert {
	return fmt.Errorf("alert %s has too many decisions (%d)", *alert.UUID, len(alert.Decisions))
}

Type guard

null

Try / catch

ids, err := client.CreateAlert(ctx, machineID, alerts)
if err != nil && strings.Contains(err.Error(), "attach decisions") {
	// tx rolled back; back off and retry whole batch
	time.Sleep(backoff)
	ids, err = client.CreateAlert(ctx, machineID, alerts)
}

Prevention

When it happens

Trigger: The AddDecisions update fails for any reason: SQLite 'database is locked' persisting beyond maxLockRetries ("exceeded N busy retries"), a foreign-key/unique constraint on decision rows, the update statement hitting a dropped connection, or context cancellation during the batches.

Common situations: High-concurrency setups on SQLite where LAPI writes and cscli queries contend and the busy-retry loop is exhausted; very large alert batches with many decisions exceeding the configured decisionBulkSize across many batches while another writer holds the lock; DB connectivity blips on MySQL/Postgres backends.

Related errors


AI-assisted analysis of crowdsecurity/crowdsec@909b515798 (2026-09-06). Data as JSON: /api/errors/6a655895e756115e. Report an issue: GitHub.