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.
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)
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)
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,javainPATH)● 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
Memory: 32 GB RAM
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("================");
}
}
