Speeding Up JDBC Bulk Inserts in YugabyteDB YSQL

High-volume inserts into YugabyteDB YSQL can be significantly faster when write batching and buffering are configured correctly. Many insert performance issues are not caused by storage limits or schema design, but by how YSQL batches and flushes writes to DocDB.

This YugabyteDB tip explains how to optimize bulk inserts in YugabyteDB using three proven configuration settings… without changing schemas, primary keys, or table layouts.

YugabyteDB Version Used

The behavior and examples in this tip were validated using YugabyteDB 2025.2 (2025.2.0.0). Configuration defaults and behavior may differ in earlier releases.

Optimize YugabyteDB Inserts with reWriteBatchedInserts

When inserting data from Java, JDBC, Hibernate, or JPA, the most important setting to enable is:

				
					reWriteBatchedInserts=true
				
			

Why this matters for YugabyteDB insert performance:

  • ● Without this setting, batched inserts are executed as many single-row INSERT statements

  • ● With it enabled, YugabyteDB rewrites batches into efficient multi-row INSERT statements:

				
					INSERT INTO table VALUES (...), (...), (...);
				
			

For high-throughput YSQL inserts, this setting is required.

Increase ysql_session_max_batch_size for Bulk Inserts

ysql_session_max_batch_size controls how many write operations YSQL buffers before flushing them to the DocDB storage layer.

The default value is often too low for bulk insert workloads.

You can inspect the current session value using:

				
					yugabyte=# SELECT setting, boot_val, short_desc FROM pg_settings WHERE name = 'ysql_session_max_batch_size';
 setting | boot_val |                                           short_desc
---------+----------+------------------------------------------------------------------------------------------------
 0       | 0        | Sets the maximum batch size for writes that YSQL can buffer before flushing to tablet servers.
(1 row)
				
			
Note about pg_settings showing 0
Although pg_settings shows a default value of 0, this indicates that YSQL is using the cluster-level default for ysql_session_max_batch_size, which is 3072 operations unless overridden. Setting the parameter explicitly at the session level replaces this default.

To increase batching for bulk inserts, set it explicitly:

				
					SET ysql_session_max_batch_size = 10000;
				
			

Benefits of increasing ysql_session_max_batch_size:

  • ● Larger buffered write batches

  • ● Fewer write RPCs to DocDB

  • ● Lower insert latency

  • ● Higher sustained insert throughput

This is often the single most impactful tuning change for bulk YSQL inserts.

Tune ysql_max_in_flight_ops Alongside Batch Size

ysql_max_in_flight_ops limits how many write operations can be outstanding before YSQL blocks and forces a flush.

Check the current value:

				
					yugabyte=# SELECT setting, boot_val, short_desc FROM pg_settings WHERE name = 'ysql_max_in_flight_ops';
 setting | boot_val |                                 short_desc
---------+----------+----------------------------------------------------------------------------
 10000   | 10000    | Maximum number of in-flight operations allowed from YSQL to tablet servers
(1 row)
				
			
Note about ysql_max_in_flight_ops default
The default value for ysql_max_in_flight_ops is 10000, as shown directly in pg_settings. Setting this parameter explicitly overrides the default for the current session.

For bulk insert workloads, increase it together with batch size:

				
					SET ysql_max_in_flight_ops = 30000;
				
			

Recommended best practice:

				
					ysql_max_in_flight_ops ≈ 3 × ysql_session_max_batch_size
				
			

This allows YSQL to pipeline writes efficiently without blocking too early.

Tune Application Insert Batch Size Gradually

On the application side (JDBC, Hibernate, JPA), increase insert batch size gradually and measure results:

				
					1024 → 2048 → 4096 → 10000
				
			

Why gradual tuning works best:

  • ● Identifies the optimal latency vs memory tradeoff

  • ● Avoids unnecessary memory pressure

  • ● Works well with YugabyteDB’s distributed write pipeline

Moderate batch sizes combined with some concurrency often outperform very large batches with low parallelism.

Benchmark: JDBC Smart Driver Bulk Insert (Real Results)

This benchmark uses a simple Java program that performs JDBC batched inserts to measure bulk insert throughput into YugabyteDB YSQL using the YugabyteDB JDBC Smart Driver.

The program intentionally focuses on the most common real-world insert pattern:

  • ● JDBC PreparedStatement

  • addBatch() / executeBatch() loops

  • ● Multiple application threads

  • ● Fixed batch size per execute

  • ● Session-level YSQL tuning applied per connection

It is specifically useful for demonstrating the performance impact of:

  • reWriteBatchedInserts=true (JDBC driver rewrite)

  • ysql_session_max_batch_size (YSQL write buffering)

  • ysql_max_in_flight_ops (outstanding write pipelining)

Each run inserts a configurable number of rows and prints total elapsed time and rows-per-second throughput.

Benchmark Table Schema

The program creates (once) and inserts into the following table:

				
					CREATE TABLE insert_test_jdbc (
  id BIGSERIAL PRIMARY KEY,
  k1 INT NOT NULL,
  k2 INT NOT NULL,
  payload TEXT NOT NULL
);

CREATE INDEX insert_test_jdbc_k1_idx ON insert_test_jdbc(k1);
CREATE INDEX insert_test_jdbc_k2_idx ON insert_test_jdbc(k2);
				
			

This schema intentionally includes indexes so that each row generates multiple write operations, making batching and buffering effects visible.

Requirements to Run the Benchmark
  • ● YugabyteDB: YSQL enabled, database yugabyte, table/index create privileges (tested on 2025.2)

  • ● Java: Java 11+ (javac, java in PATH)

  • ● Driver: YugabyteDB JDBC Smart Driver (for example jdbc-yugabytedb-42.7.3-yb-4.jar)

  • ● Source: YBInsertBenchmark.java (single file) (Code provided later)

Example Usage
Test Environment
CPU: 8 vCPUs
Memory: 32 GB RAM
Why this matters: insert throughput is influenced by available CPU and memory, so results will vary based on hardware and VM sizing.

Baseline Run (No Rewrite, Defaults)

				
					java -cp .:jdbc-yugabytedb-42.7.3-yb-4.jar YBInsertBenchmark \
  --url "jdbc:yugabytedb://127.0.0.1:5433/yugabyte" \
  --user yugabyte \
  --password yugabyte \
  --rows 500000 \
  --batchSize 512 \
  --threads 4 \
  --payloadBytes 200 \
  --ybBatch 0 \
  --ybInFlight 0 \
  --create \
  --truncate
				
			

Sample output:

				
					==== YugabyteDB JDBC Insert Benchmark ====
URL: jdbc:yugabytedb://127.0.0.1:5433/yugabyte
Rows: 500000
Threads: 4
JDBC batch size: 512
Session overrides: ysql_session_max_batch_size=0, ysql_max_in_flight_ops=0
=========================================

==== Results ====
Inserted: 500,000 rows
Elapsed:  137.122 seconds
Rate:     3.65 K rows/sec
================
				
			

Optimized Run (Rewrite + YSQL Tuning)

				
					java -cp .:jdbc-yugabytedb-42.7.3-yb-4.jar YBInsertBenchmark \
  --url "jdbc:yugabytedb://127.0.0.1:5433/yugabyte?reWriteBatchedInserts=true" \
  --user yugabyte \
  --password yugabyte \
  --rows 500000 \
  --batchSize 512 \
  --threads 4 \
  --payloadBytes 200 \
  --ybBatch 10000 \
  --ybInFlight 30000 \
  --truncate
				
			

Sample output:

				
					==== YugabyteDB JDBC Insert Benchmark ====
URL: jdbc:yugabytedb://127.0.0.1:5433/yugabyte?reWriteBatchedInserts=true
Rows: 500000
Threads: 4
JDBC batch size: 512
Session overrides: ysql_session_max_batch_size=10000, ysql_max_in_flight_ops=30000
=========================================

==== Results ====
Inserted: 500,000 rows
Elapsed:  14.438 seconds
Rate:     34.63 K rows/sec
================
				
			
Bulk Insert Performance (Baseline vs Optimized)
Configuration Rows Time (seconds) Throughput
Baseline (no rewrite, defaults) 500,000 137.122 3.65K rows/sec
Optimized (rewrite + tuned YSQL) 500,000 14.438 34.63K rows/sec

Improvement: 9.49× faster (about 89.5% less time) for this JDBC bulk insert workload!

Summary

High-volume inserts into YugabyteDB YSQL can be significantly faster when JDBC batching and YSQL write buffering are configured correctly. For Java-based workloads, the biggest gains come from enabling JDBC batched insert rewriting and allowing YSQL to buffer and pipeline more write operations before flushing to storage.

In this benchmark, using the YugabyteDB JDBC Smart Driver with reWriteBatchedInserts=true, along with tuning ysql_session_max_batch_size and ysql_max_in_flight_ops, reduced total insert time by nearly an order of magnitude… without changing schemas, primary keys, or table layouts.

These settings are session-scoped, easy to test, and safe to roll back, making them an effective first step when troubleshooting or optimizing bulk insert performance in YugabyteDB.

Have Fun!

Source: YBInsertBenchmark.java

				
					import java.sql.*;
import java.time.Duration;
import java.time.Instant;
import java.util.Properties;
import java.util.concurrent.ThreadLocalRandom;

/**
 * YugabyteDB JDBC insert benchmark using the YugabyteDB Smart Driver.
 *
 * Demonstrates:
 *  - JDBC batching
 *  - reWriteBatchedInserts=true
 *  - ysql_session_max_batch_size
 *  - ysql_max_in_flight_ops
 *
 * Compile:
 *   javac -cp yugabyte-jdbc-42.7.4-yb-1.12.jar YBInsertBenchmark.java
 *
 * Run:
 *   java -cp .:yugabyte-jdbc-42.7.4-yb-1.12.jar YBInsertBenchmark ...
 */
public class YBInsertBenchmark {

  /* ---------------- Argument helpers ---------------- */

  private static String getArg(String[] args, String name, String def) {
    for (int i = 0; i < args.length; i++) {
      if (args[i].equals(name) && i + 1 < args.length) {
        return args[i + 1];
      }
    }
    return def;
  }

  private static boolean hasFlag(String[] args, String name) {
    for (String a : args) {
      if (a.equals(name)) return true;
    }
    return false;
  }

  /* ---------------- Schema helpers ---------------- */

  private static void createSchema(Connection c) throws SQLException {
    try (Statement st = c.createStatement()) {
      st.execute("""
        CREATE TABLE IF NOT EXISTS insert_test_jdbc (
          id BIGSERIAL PRIMARY KEY,
          k1 INT NOT NULL,
          k2 INT NOT NULL,
          payload TEXT NOT NULL
        )
      """);

      st.execute("""
        CREATE INDEX IF NOT EXISTS insert_test_jdbc_k1_idx
        ON insert_test_jdbc(k1)
      """);

      st.execute("""
        CREATE INDEX IF NOT EXISTS insert_test_jdbc_k2_idx
        ON insert_test_jdbc(k2)
      """);
    }
  }

  private static void truncateTable(Connection c) throws SQLException {
    try (Statement st = c.createStatement()) {
      st.execute("TRUNCATE TABLE insert_test_jdbc");
    }
  }

  /* ---------------- YB session tuning ---------------- */

  private static void applySessionSettings(Connection c, int batchSize, int inFlight)
      throws SQLException {
    try (Statement st = c.createStatement()) {
      if (batchSize > 0) {
        st.execute("SET ysql_session_max_batch_size = " + batchSize);
      }
      if (inFlight > 0) {
        st.execute("SET ysql_max_in_flight_ops = " + inFlight);
      }
    }
  }

  /* ---------------- Worker ---------------- */

  private static final class Worker implements Runnable {
    private final String url;
    private final Properties props;
    private final long rows;
    private final int jdbcBatch;
    private final int payloadBytes;
    private final int ybBatch;
    private final int ybInFlight;

    volatile long inserted = 0;
    volatile Exception error = null;

    Worker(
        String url,
        Properties props,
        long rows,
        int jdbcBatch,
        int payloadBytes,
        int ybBatch,
        int ybInFlight) {

      this.url = url;
      this.props = props;
      this.rows = rows;
      this.jdbcBatch = jdbcBatch;
      this.payloadBytes = payloadBytes;
      this.ybBatch = ybBatch;
      this.ybInFlight = ybInFlight;
    }

    @Override
    public void run() {
      final String sql =
          "INSERT INTO insert_test_jdbc(k1, k2, payload) VALUES (?, ?, ?)";
      final ThreadLocalRandom rnd = ThreadLocalRandom.current();
      final String payload = "x".repeat(Math.max(1, payloadBytes));

      try (Connection c = DriverManager.getConnection(url, props)) {
        c.setAutoCommit(false);
        applySessionSettings(c, ybBatch, ybInFlight);

        try (PreparedStatement ps = c.prepareStatement(sql)) {
          long local = 0;
          int batchCount = 0;

          while (local < rows) {
            ps.setInt(1, rnd.nextInt(0, 1000));
            ps.setInt(2, rnd.nextInt(0, 2000));
            ps.setString(3, payload);

            ps.addBatch();
            batchCount++;
            local++;
            inserted = local;

            if (batchCount >= jdbcBatch) {
              ps.executeBatch();
              c.commit();
              batchCount = 0;
            }
          }

          if (batchCount > 0) {
            ps.executeBatch();
            c.commit();
          }
        }
      } catch (Exception e) {
        error = e;
      }
    }
  }

  private static String rate(double rps) {
    if (rps > 1_000_000) return String.format("%.2f M rows/sec", rps / 1_000_000);
    if (rps > 1_000) return String.format("%.2f K rows/sec", rps / 1_000);
    return String.format("%.2f rows/sec", rps);
  }

  /* ---------------- Main ---------------- */

  public static void main(String[] args) throws Exception {

    // Force-load YugabyteDB Smart Driver
    Class.forName("com.yugabyte.Driver");

    String url = getArg(args, "--url", null);
    String user = getArg(args, "--user", "yugabyte");
    String password = getArg(args, "--password", "yugabyte");

    long rows = Long.parseLong(getArg(args, "--rows", "500000"));
    int jdbcBatch = Integer.parseInt(getArg(args, "--batchSize", "512"));
    int threads = Integer.parseInt(getArg(args, "--threads", "4"));
    int payloadBytes = Integer.parseInt(getArg(args, "--payloadBytes", "200"));

    int ybBatch = Integer.parseInt(getArg(args, "--ybBatch", "0"));
    int ybInFlight = Integer.parseInt(getArg(args, "--ybInFlight", "0"));

    boolean create = hasFlag(args, "--create");
    boolean truncate = hasFlag(args, "--truncate");

    if (url == null) {
      System.err.println("Missing --url");
      System.exit(2);
    }

    Properties props = new Properties();
    props.setProperty("user", user);
    props.setProperty("password", password);

    if (create || truncate) {
      try (Connection c = DriverManager.getConnection(url, props)) {
        if (create) createSchema(c);
        if (truncate) truncateTable(c);
      }
    }

    long perThread = rows / threads;
    long remainder = rows % threads;


    Worker[] workers = new Worker[threads];
    Thread[] threadsArr = new Thread[threads];

    for (int i = 0; i < threads; i++) {
      long r = perThread + (i == 0 ? remainder : 0);
      workers[i] =
          new Worker(url, props, r, jdbcBatch, payloadBytes, ybBatch, ybInFlight);
      threadsArr[i] = new Thread(workers[i], "worker-" + i);
    }

    System.out.println("==== YugabyteDB JDBC Insert Benchmark ====");
    System.out.println("URL: " + url);
    System.out.println("Rows: " + rows);
    System.out.println("Threads: " + threads);
    System.out.println("JDBC batch size: " + jdbcBatch);
    System.out.println(
        "Session overrides: ysql_session_max_batch_size="
            + ybBatch
            + ", ysql_max_in_flight_ops="
            + ybInFlight);
    System.out.println("=========================================");

    Instant start = Instant.now();
    for (Thread t : threadsArr) t.start();
    for (Thread t : threadsArr) t.join();
    Instant end = Instant.now();

    for (Worker w : workers) {
      if (w.error != null) throw w.error;
    }

    long total = 0;
    for (Worker w : workers) total += w.inserted;

    double seconds =
        Math.max(0.001, Duration.between(start, end).toMillis() / 1000.0);

    System.out.println("\n==== Results ====");
    System.out.printf("Inserted: %,d rows%n", total);
    System.out.printf("Elapsed:  %.3f seconds%n", seconds);
    System.out.printf("Rate:     %s%n", rate(total / seconds));
    System.out.println("================");
  }
}
				
			
Exploring the trails around the Golden Gate Bridge, where the views are stunning and the wildlife is… interactive!