Interlace ESLint
ESLint Interlace
PostgreSQLRules

no-batch-insert-loop

Prevents INSERT/UPDATE/DELETE queries inside loops (N+1 query anti-pattern).

Keywords: N+1 queries, performance, CWE-400, pg, node-postgres, bulk operations

Prevents INSERT/UPDATE/DELETE queries inside loops (N+1 query anti-pattern).

⚠️ This rule errors by default in the recommended config.

Quick Summary

AspectDetails
CWE ReferenceCWE-400 (Resource Exhaustion)
SeverityMedium (performance)
CategoryPerformance

Rule Details

Executing mutation queries in loops creates N+1 query problems, causing:

  • Database connection exhaustion
  • Slow execution times
  • Transaction timeout issues

❌ Incorrect

// N+1 inserts
for (const user of users) {
  await client.query('INSERT INTO users VALUES ($1, $2)', [user.id, user.name]);
}

// forEach with mutations
users.forEach(async (user) => {
  await pool.query('UPDATE users SET active = true WHERE id = $1', [user.id]);
});

// map with mutations
users.map((user) => client.query('INSERT INTO users VALUES ($1)', [user.id]));

✅ Correct

// Bulk insert with unnest
await client.query(
  'INSERT INTO users SELECT * FROM unnest($1::int[], $2::text[])',
  [users.map((u) => u.id), users.map((u) => u.name)],
);

// pg-format for batching
const values = users.map((u) => [u.id, u.name]);
await client.query(format('INSERT INTO users VALUES %L', values));

// ANY for bulk updates
await client.query('UPDATE users SET active = true WHERE id = ANY($1)', [
  userIds,
]);

Error Message Format

The rule provides LLM-optimized error messages (Compact 2-line format) with actionable security guidance:

🔒 CWE-400 OWASP:A06 CVSS:7.5 | Uncontrolled Resource Consumption (ReDoS) detected | HIGH
   Fix: Review and apply the recommended fix | https://owasp.org/Top10/A06_2021/

Message Components

ComponentPurposeExample
Risk StandardsSecurity benchmarksCWE-400 OWASP:A06 CVSS:7.5
Issue DescriptionSpecific vulnerabilityUncontrolled Resource Consumption (ReDoS) detected
Severity & ComplianceImpact assessmentHIGH
Fix InstructionActionable remediationFollow the remediation steps below
Technical TruthOfficial referenceOWASP Top 10

Known False Negatives

The following patterns are not detected due to static analysis limitations:

Async Iterators

Why: The rule doesn't traverse for await...of as a loop type.

// ❌ NOT DETECTED
async function* getUsers() {
  yield* users;
}
for await (const user of getUsers()) {
  await client.query('INSERT INTO users VALUES ($1)', [user.id]);
}

Recursive Functions

Why: Recursion creates implicit loops that require call graph analysis.

// ❌ NOT DETECTED
async function insertUsers(users: User[], i = 0): Promise<void> {
  if (i >= users.length) return;
  await client.query('INSERT INTO users VALUES ($1)', [users[i].id]);
  return insertUsers(users, i + 1); // Recursive N+1!
}

Promise.all with Mapping

Why: The rule only detects direct .map() callbacks, not when wrapped in Promise.all.

// ❌ NOT DETECTED - Array.from pattern
const queries = Array.from(users, (u) =>
  client.query('INSERT INTO users VALUES ($1)', [u.id]),
);
await Promise.all(queries);

External Loop Functions

Why: Queries called from a helper function inside a loop are not detected.

// ❌ NOT DETECTED
async function insertUser(user: User) {
  await client.query('INSERT INTO users VALUES ($1)', [user.id]);
}
for (const user of users) {
  await insertUser(user); // N+1 but rule can't see into helper
}

Workaround: Use UNNEST() for bulk inserts or ANY() for bulk updates.

When Not To Use It

  • When processing results in a streaming fashion with backpressure
  • For operations that must be transactionally isolated per item
  • When using a batching library (e.g., DataLoader) that coalesces queries

On this page