node.js
mySQL
bulk insert
database operations
javascript programming

How do I do a bulk insert in mySQL using node.js

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Bulk inserting into MySQL from Node.js is about reducing round trips and sending many rows in one statement or a small number of batched statements. The main goal is to avoid inserting one row per query. In practice, the best solution is usually a parameterized multi-row INSERT, optionally wrapped in a transaction and split into batches if the dataset is large.

The Basic Multi-Row Insert Pattern

Suppose you want to insert several users at once. A reliable approach is to generate one placeholder group per row and flatten the values.

javascript
1const mysql = require("mysql2/promise");
2
3async function bulkInsertUsers() {
4  const conn = await mysql.createConnection({
5    host: "127.0.0.1",
6    user: "app",
7    password: "secret",
8    database: "demo"
9  });
10
11  const rows = [
12    ["Ava", "[email protected]"],
13    ["Noah", "[email protected]"],
14    ["Mia", "[email protected]"]
15  ];
16
17  const placeholders = rows.map(() => "(?, ?)").join(", ");
18  const values = rows.flat();
19
20  const sql = `INSERT INTO users (name, email) VALUES ${placeholders}`;
21  await conn.execute(sql, values);
22
23  await conn.end();
24}
25
26bulkInsertUsers().catch(console.error);

This is explicit, parameterized, and easy to reason about.

Why This Is Better Than One Insert Per Row

Doing this:

javascript
// loop rows and run one INSERT for each row

creates a large amount of avoidable overhead:

  • more client-server round trips
  • more parse and execution overhead
  • higher transaction cost
  • slower total throughput

For moderate-sized datasets, a single multi-row insert is usually dramatically better.

Use Batches for Large Inputs

You should not blindly put hundreds of thousands of rows into one giant SQL string. Large payloads can hit packet limits or create excessive memory pressure. A common solution is to chunk the rows.

javascript
1function chunk(array, size) {
2  const result = [];
3  for (let i = 0; i < array.length; i += size) {
4    result.push(array.slice(i, i + size));
5  }
6  return result;
7}
8
9async function bulkInsertInBatches(conn, rows, batchSize = 1000) {
10  for (const batch of chunk(rows, batchSize)) {
11    const placeholders = batch.map(() => "(?, ?)").join(", ");
12    const values = batch.flat();
13    const sql = `INSERT INTO users (name, email) VALUES ${placeholders}`;
14    await conn.execute(sql, values);
15  }
16}

Batching is usually the right balance between throughput and safety.

If all batches belong to one logical operation, use a transaction.

javascript
1const mysql = require("mysql2/promise");
2
3async function insertUsers(rows) {
4  const conn = await mysql.createConnection({
5    host: "127.0.0.1",
6    user: "app",
7    password: "secret",
8    database: "demo"
9  });
10
11  try {
12    await conn.beginTransaction();
13    await bulkInsertInBatches(conn, rows, 1000);
14    await conn.commit();
15  } catch (err) {
16    await conn.rollback();
17    throw err;
18  } finally {
19    await conn.end();
20  }
21}

This gives you atomicity and usually better performance characteristics than many auto-committed insert statements.

Handle Duplicates Intentionally

Bulk insert logic often collides with existing unique keys. Decide the policy explicitly.

Insert and fail on duplicates:

sql
INSERT INTO users (name, email) VALUES ...

Ignore duplicates:

sql
INSERT IGNORE INTO users (name, email) VALUES ...

Upsert:

sql
INSERT INTO users (name, email) VALUES ...
ON DUPLICATE KEY UPDATE name = VALUES(name)

Do not leave this to accident. Duplicate-key behavior is part of the API contract of the insert operation.

Validate Input Before Building SQL

Even with parameterized placeholders, garbage input still causes bad data. Validate shape and business rules before sending the insert.

javascript
1function validateRow(row) {
2  return Array.isArray(row) &&
3    row.length === 2 &&
4    typeof row[0] === "string" &&
5    typeof row[1] === "string";
6}
7
8const cleanedRows = rows.filter(validateRow);

Parameters protect against SQL injection, but they do not protect against malformed business data.

About VALUES ?

Some Node MySQL examples use a shorthand like:

javascript
// INSERT INTO users (name, email) VALUES ?

That style can work with certain drivers and query-formatting behaviors, but it is less explicit and easier to misunderstand across libraries or prepared-statement APIs. Generating placeholder groups directly is more portable and easier to audit.

When LOAD DATA Is Better

For truly massive imports, MySQL’s bulk-loading features such as LOAD DATA INFILE or LOAD DATA LOCAL INFILE can outperform application-driven multi-row inserts by a large margin. If the task is large-scale ingestion rather than ordinary application writes, it is worth considering database-native import tools instead of stretching the app-layer insert loop too far.

Common Pitfalls

The biggest mistake is inserting one row at a time in a loop. Another is trying to send an excessively huge single statement instead of batching. Developers also often forget transaction boundaries, which leads to partial success across multiple batches. Finally, duplicate-key behavior should be chosen intentionally rather than discovered by production errors.

Summary

  • Use a parameterized multi-row INSERT for normal bulk insert work in Node.js.
  • Build explicit placeholder groups and flatten the values array.
  • Batch large datasets instead of sending one enormous statement.
  • Wrap related inserts in a transaction when atomicity matters.
  • For extremely large imports, consider MySQL-native bulk loading instead of application-level inserts.

Course illustration
Course illustration

All Rights Reserved.