crowdsecurity/crowdsec · error

unable to create alert: %w

Error message

unable to create alert: %w

What it means

After the lookup shows the alert is absent, CreateOrUpdateAlert calls CreateAlert to insert it (with its events and decisions, in bulk transactions). If CreateAlert fails for any reason, the error is wrapped as 'unable to create alert: %w'. The underlying cause is usually a transactional insert failure surfaced through rollbackOnError (error 890).

Source

Thrown at pkg/database/alerts.go:59

// CreateOrUpdateAlert is specific to PAPI : It checks if alert already exists, otherwise inserts it
// if alert already exists, it checks it associated decisions already exists
// if some associated decisions are missing (ie. previous insert ended up in error) it inserts them
func (c *Client) CreateOrUpdateAlert(ctx context.Context, machineID string, alertItem *models.Alert) (string, error) {
	if alertItem.UUID == "" {
		return "", errors.New("alert UUID is empty")
	}

	alerts, err := c.Ent.Alert.Query().Where(alert.UUID(alertItem.UUID)).WithDecisions().All(ctx)
	if err != nil && !ent.IsNotFound(err) {
		return "", fmt.Errorf("unable to query alerts for uuid %s: %w", alertItem.UUID, err)
	}

	// alert wasn't found, insert it (expected hotpath)
	if ent.IsNotFound(err) || len(alerts) == 0 {
		alertIDs, err := c.CreateAlert(ctx, machineID, []*models.Alert{alertItem})
		if err != nil {
			return "", fmt.Errorf("unable to create alert: %w", err)
		}

		// happy nilaway
		if len(alertIDs) == 0 {
			return "", fmt.Errorf("unable to create alert: no IDs returned for alert %s", alertItem.UUID)
		}

		return alertIDs[0], nil
	}

	// this should never happen
	if len(alerts) > 1 {
		return "", fmt.Errorf("multiple alerts found for uuid %s", alertItem.UUID)
	}

	log.Infof("Alert %s already exists, checking associated decisions", alertItem.UUID)

	// alert is found, check for any missing decisions

View on GitHub (pinned to 909b515798)

Solutions

  1. Unwrap the error to identify the CreateAlert cause (lock, constraint, validation)
  2. For SQLite lock errors, stagger writers or migrate to PostgreSQL/MySQL
  3. Validate the alert payload (decision UUIDs, scope/value fields) before pushing
  4. Retry: the lookup-by-UUID makes the operation idempotent, so a retry after a transient failure is safe

Example fix

// before: fire-and-forget push
// after: retry transient insert failures
id, err := client.CreateOrUpdateAlert(ctx, machineID, alert)
if err != nil && isTransient(err) {
    time.Sleep(backoff)
    id, err = client.CreateOrUpdateAlert(ctx, machineID, alert)
}
Defensive patterns

Strategy: retry

Validate before calling

// validate payload before insert
for _, d := range alertItem.Decisions {
    if d.UUID == "" || d.Value == nil || *d.Value == "" {
        return errors.New("invalid decision in alert payload")
    }
}
if alertItem.UUID == "" { return errors.New("alert UUID is empty") }

Try / catch

id, err := client.CreateOrUpdateAlert(ctx, machineID, alert)
if err != nil && strings.HasPrefix(err.Error(), "unable to create alert:") && !strings.Contains(err.Error(), "no IDs") {
    time.Sleep(retryBackoff)
    id, err = client.CreateOrUpdateAlert(ctx, machineID, alert) // idempotent by UUID
}

Prevention

When it happens

Trigger: c.CreateAlert(ctx, machineID, []*models.Alert{alertItem}) returns an error during a PAPI alert push — typically a failed bulk insert (DB locked, constraint violation, oversized payload, malformed decision source range) — while the alert UUID was confirmed not already present.

Common situations: Concurrent LAPI writes locking SQLite; alert payload with invalid IP/range strings in decisions; decisions referencing malformed metadata; disk full; version mismatch causing schema mismatch during insert.

Related errors


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