crowdsecurity/crowdsec · error · BulkError
bulk creating alert: %w: %w
Error message
bulk creating alert: %w: %w
What it means
saveAlerts performs a single ent CreateBulk insert of all alert builders inside a transaction and wraps any database error with the BulkError sentinel. This is the standard bulk-insert failure path: the database rejected one or more of the alert rows (constraint violation, size limit, connection drop, etc.), so none of the batch is saved and the caller rolls back the transaction.
Source
Thrown at pkg/database/alerts.go:603
func (c *Client) saveAlerts(ctx context.Context, client *ent.Client, batch []alertCreatePlan) ([]string, error) {
if len(batch) == 0 {
log.Warningf("no alerts to create, discarded?")
return nil, nil
}
// extract builders in the same order
builders := make([]*ent.AlertCreate, len(batch))
for i := range batch {
if batch[i].builder == nil {
return nil, fmt.Errorf("nil alert builder at index %d", i)
}
builders[i] = batch[i].builder
}
alertsCreateBulk, err := client.Alert.CreateBulk(builders...).Save(ctx)
if err != nil {
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)View on GitHub (pinned to 909b515798)
Solutions
- Read the wrapped underlying error (errors.Unwrap / %v of the chain) to identify the specific DB failure — constraints, lock, connection, or context
- If it is a duplicate/UUID constraint, stop re-submitting the same alert or deduplicate by UUID before calling CreateAlert
- If it is sqlite busy/locked, check for concurrent writers (other crowdsec/cscli processes) and ensure the DB is on a local filesystem, not NFS
- If it is a connection failure, verify DB reachability (host/port/credentials in crowdsec.yaml) and retry; the transaction was rolled back so the operation is safe to repeat
- If a field is too large, truncate or limit the alert payload (scenario, message) before ingestion
Example fix
// before
ids, err := client.CreateAlert(ctx, machineID, alerts)
if err != nil {
return err
}
// after
ids, err := client.CreateAlert(ctx, machineID, dedupeByUUID(alerts))
if err != nil {
if database.IsBulkError(err) {
log.Errorf("bulk alert insert failed: %v", errors.Unwrap(err))
}
return err
} Defensive patterns
Strategy: retry
Validate before calling
// dedupe and sanity-check alerts before submission
seen := map[string]bool{}
for _, a := range alerts {
if a.UUID == nil || seen[*a.UUID] {
return fmt.Errorf("missing or duplicate alert UUID")
}
seen[*a.UUID] = true
} Type guard
func validAlert(a *models.Alert) bool {
return a != nil && a.UUID != nil && a.Scenario != nil && a.Source != nil && a.Source.Scope != nil && a.Source.Value != nil
} Try / catch
ids, err := client.CreateAlert(ctx, machineID, alerts)
var bulkErr *database.BulkError
if err != nil {
if errors.As(err, &bulkErr) || database.IsBulkError(err) {
log.Errorf("bulk insert rejected: %v", err) // inspect wrapped DB cause before retry
}
} Prevention
- Deduplicate alerts by UUID before ingestion
- Keep the crowdsec SQLite DB on a local disk, never NFS
- Monitor disk space on the database volume
- Validate alert payloads (all required pointer fields set) before calling CreateAlert
- On MySQL/Postgres, monitor connection health and max_connections
When it happens
Trigger: client.Alert.CreateBulk(builders...).Save(ctx) returns an error — e.g. a NOT NULL/UNIQUE constraint failure (duplicate alert UUID), a field value exceeding column size, SQLite 'database is locked'/'disk I/O error', MySQL/Postgres connection loss, or context cancellation mid-insert.
Common situations: Duplicate alert UUID when the same alert is submitted twice to a SQLite LAPI database; oversized scenario/message values against a fixed-size column on MySQL; sqlite database file locked by a concurrent cscli command or bouncer writes; disk full on the database volume.
Related errors
- unable to expire decisions for list %s : %w
- %s: %w
- committing alert transaction: %w: %w
- unable to add values to allowlist: %w
- creating machine '%s': %w
AI-assisted analysis of crowdsecurity/crowdsec@909b515798 (2026-09-06).
Data as JSON: /api/errors/afacab04897933ee.
Report an issue: GitHub.