A GORM plugin that automatically switches between individual and batch database operations based on measured P95 latency.
When latency is high, operations are buffered and flushed as a single transaction — reducing round-trips. When latency is low, operations are sent individually with no overhead.
- Tracks operation latency using a P95 sliding window (Prometheus-style bucket ring, last 30s by default)
- When
P95 > LatencyThreshold→ batch mode: operations are buffered and flushed together - When
P95 ≤ LatencyThreshold→ individual mode: operations pass through normally - Flush triggers: elapsed time or buffer size, whichever comes first
- Batch semantics are all-or-nothing inside a single transaction
go get github.com/adrielcodeco/gorm-autobatchRequires Go 1.21+ and GORM v1.30+
import (
autobatch "github.com/adrielcodeco/gorm-autobatch"
"gorm.io/gorm"
)
db, err := gorm.Open(...)
threshold := 50 * time.Millisecond
err = db.Use(autobatch.New(autobatch.Config{
LatencyThreshold: &threshold, // nil = disabled; 0 = always batch; >0 = adaptive
FlushTimeout: 10 * time.Millisecond, // flush batch after 10ms idle
MaxBatchSize: 100, // or when 100 ops are buffered
WindowDuration: 30 * time.Second, // P95 measured over last 30s
}))
// Regular GORM calls — the plugin decides whether to batch transparently.
db.Create(&user)
db.Model(&user).Updates(&payload)
db.Delete(&record)| Field | Default | Description |
|---|---|---|
LatencyThreshold |
nil |
P95 above this switches to batch mode (nil disables batching) |
FlushTimeout |
10ms |
Max wait before flushing a partial batch |
MaxBatchSize |
100 |
Max ops per batch before forced flush |
WindowDuration |
30s |
Sliding window duration for P95 measurement |
All fields are optional — zero values use the defaults above.
db.Create()db.Updates()db.Delete()
Queries (Find, First, etc.) are not batched.
All operations in a batch run inside a single transaction. Each op is
wrapped in its own SAVEPOINT, so a per-op failure (e.g. a unique-constraint
violation) is isolated: only the failing caller gets the error, the rest of
the batch still commits.
Infrastructure failures of the outer transaction (BEGIN/COMMIT, connection loss, savepoint unsupported) propagate to every caller in the batch.
Callers block transparently until their batch is flushed — from the caller's perspective it looks like a normal synchronous GORM call.
- Operations inside
db.Transaction(...)ordb.Begin()are never batched. They run inline on the user's transaction to preserve atomicity and rollback semantics. The plugin detects this automatically. - Callbacks registered after
gorm:create/gorm:update/gorm:deleteare skipped for batched ops. Internally the plugin setsDryRun = trueafter the batch executes the op via a separate session, which short-circuits the rest of the callback chain on the caller's*gorm.DB. Hooks declared on the model (e.g.AfterCreate) do run, but inside the batch session — not on the caller's*gorm.DB. RowsAffectedis propagated back to the caller's*gorm.DBafter the batch executes, but otherStatementfields are not.- Graceful shutdown: call
plugin.Close()before exiting your process to drain in-flight batches. After Close, intercepted ops returnErrBatcherClosed.
MIT