Billionmail/BillionMail · error

failed to load contacts: %v

Error message

failed to load contacts: %v

What it means

preloadData fetches bm_contacts rows for all recipient emails to build the in-memory contact cache. If the SELECT ... WHERE email IN (...) on bm_contacts fails, it wraps the driver error as "failed to load contacts: %v". Missing contacts are not an error here; only an actual DB read failure triggers this message.

Source

Thrown at core/internal/service/batch_mail/api_mail_send.go:395

		templateIds = append(templateIds, t.TemplateId)
	}
	// Batch query email templates
	var emailTemplates []entity.EmailTemplate
	err = g.DB().Model("email_templates").
		WhereIn("id", templateIds).
		Ctx(ctx).
		Scan(&emailTemplates)
	if err != nil {
		return nil, fmt.Errorf("failed to load email templates: %v", err)
	}
	// Batch query contacts
	var contacts []entity.Contact
	err = g.DB().Model("bm_contacts").
		WhereIn("email", recipientEmails).
		Ctx(ctx).
		Scan(&contacts)
	if err != nil {
		return nil, fmt.Errorf("failed to load contacts: %v", err)
	}
	// Build cache
	cache := &CacheData{
		ApiTemplates:   make(map[int]entity.ApiTemplates),
		EmailTemplates: make(map[int]entity.EmailTemplate),
		Contacts:       make(map[string]entity.Contact),
	}

	for _, t := range apiTemplates {
		cache.ApiTemplates[t.Id] = t
	}
	for _, t := range emailTemplates {
		cache.EmailTemplates[t.Id] = t
	}
	for _, c := range contacts {
		cache.Contacts[c.Email] = c
	}

View on GitHub (pinned to fc36c76c05)

Solutions

  1. Check the wrapped underlying error and DB health
  2. Chunk the recipient email list into smaller batches for the WHERE IN query
  3. Verify SELECT permission on bm_contacts and that the table schema matches the entity
  4. Add a timeout/lock diagnosis if the query times out on large lists

Example fix

// before: one huge WHERE IN
err = g.DB().Model("bm_contacts").WhereIn("email", recipientEmails).Ctx(ctx).Scan(&contacts)
// after: chunked loads
const chunk = 500
for i := 0; i < len(recipientEmails); i += chunk {
    end := i + chunk
    if end > len(recipientEmails) { end = len(recipientEmails) }
    var part []entity.Contact
    if err = g.DB().Model("bm_contacts").WhereIn("email", recipientEmails[i:end]).Ctx(ctx).Scan(&part); err != nil {
        return nil, fmt.Errorf("failed to load contacts: %v", err)
    }
    contacts = append(contacts, part...)
}
Defensive patterns

Strategy: validation

Validate before calling

const maxIn = 1000
if len(recipientEmails) > maxIn {
    return nil, fmt.Errorf("recipient list too large for single query: %d", len(recipientEmails))
}

Try / catch

cache, err := preloadData(ctx, apiIds)
if err != nil && strings.Contains(err.Error(), "failed to load contacts") {
    // chunk recipients and retry, or fail the batch gracefully
    return err
}

Prevention

When it happens

Trigger: ProcessApiMailQueue -> preloadData when the bm_contacts query errors: DB outage, prepared statement failure with a very large recipient list (e.g. WHERE IN with thousands of emails exceeding parameter limits), timeout, or permission denial.

Common situations: Very large recipient batches producing oversized WHERE IN clauses; DB connection dropped between the template and contact queries; stale indexes/locks slowing the query past timeout.

Understand the failure class

Background: Database query failed: Internal Server Error 500s wrapping SQL, Prisma, and connection failures — what to check first — this error's family across 16 libraries.

Related errors


AI-assisted analysis of Billionmail/BillionMail@fc36c76c05 (2026-09-05). Data as JSON: /api/errors/a43d268a981a4785. Report an issue: GitHub.