crowdsecurity/crowdsec · error · BulkError

committing alert transaction: %w: %w

Error message

committing alert transaction: %w: %w

What it means

At the end of createAlertBatch, the transaction containing all alert inserts, events, metas, and decision links is committed. This error wraps a commit failure with the BulkError sentinel. All work succeeded at the statement level but the database refused to make it durable (e.g. commit-time lock conflict, connection loss, deferred constraint failure, disk I/O error).

Source

Thrown at pkg/database/alerts.go:718

		if owner != nil {
			builder.SetOwner(owner)
		}

		batch = append(batch, alertCreatePlan{
			builder:   builder,
			decisions: decisions,
		})
	}

	// Save alerts, then attach decisions with retry logic
	ids, err := c.saveAlerts(ctx, txEnt, batch)
	if err != nil {
		return nil, rollbackOnError(tx, err, "saving alerts")
	}

	if err := tx.Commit(); err != nil {
		return nil, fmt.Errorf("committing alert transaction: %w: %w", err, BulkError)
	}

	return ids, nil
}

func (c *Client) CreateAlert(ctx context.Context, machineID string, alertList []*models.Alert) ([]string, error) {
	var (
		owner *ent.Machine
		err   error
	)

	if machineID != "" {
		owner, err = c.QueryMachineByID(ctx, machineID)
		if err != nil {
			if !errors.Is(err, UserNotExists) {
				return nil, fmt.Errorf("machine '%s': %w", machineID, err)
			}

View on GitHub (pinned to 909b515798)

Solutions

  1. Read the wrapped driver error to distinguish lock, connection, or I/O failure
  2. Simply retry CreateAlert — the commit rolled back, so the batch was never persisted and resubmission is safe
  3. For SQLite lock errors, enable WAL journal mode and keep the DB on local disk; or migrate to MySQL/Postgres for high-concurrency setups
  4. For connection errors, check DB server logs and network stability; adjust idle timeouts (e.g. MySQL wait_timeout) for long-running crowdsec processes
  5. Free disk space / check I/O health on the database volume

Example fix

// before
ids, err := client.CreateAlert(ctx, machineID, alerts)
if err != nil {
	panic(err)
}

// after
ids, err := client.CreateAlert(ctx, machineID, alerts)
if err != nil {
	if isRetryableDBError(err) { // lock/connection failures: tx rolled back, safe to retry
		ids, err = client.CreateAlert(ctx, machineID, alerts)
	}
	if err != nil {
		return fmt.Errorf("alert creation failed: %w", err)
	}
}
Defensive patterns

Strategy: retry

Validate before calling

if err := ctx.Err(); err != nil {
	return fmt.Errorf("context cancelled before commit: %w", err)
}

Type guard

null

Try / catch

ids, err := client.CreateAlert(ctx, machineID, alerts)
if err != nil && strings.Contains(err.Error(), "committing alert transaction") {
	// commit failed but tx rolled back; safe to retry after backoff
	time.Sleep(backoff)
	ids, err = client.CreateAlert(ctx, machineID, alerts)
}

Prevention

When it happens

Trigger: tx.Commit() returns an error: SQLite 'database is locked' at commit time under concurrent writers, connection dropped between the last statement and COMMIT on MySQL/Postgres, disk-full or I/O error while flushing, or a constraint deferred to commit time.

Common situations: SQLite contention when bouncers and cscli write simultaneously and the commit races with another writer's lock; MySQL 'server has gone away' due to wait_timeout on a long batch; disk full on the DB volume precisely at commit; network partition to a remote database during large batches.

Related errors


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