litedb-org/LiteDB · error · LiteException
0
0
Error message
Maximum number of transactions reached
What it means
Thrown by TransactionMonitor.GetTransaction when the count of concurrently open transactions reaches MAX_OPEN_TRANSACTIONS (100). LiteDB caps concurrent transactions to bound memory (each gets MAX_TRANSACTION_SIZE/MAX_OPEN_TRANSACTIONS pages). The check runs under a lock on the _transactions dictionary before a new TransactionService is created.
Source
Thrown at LiteDB/Engine/Services/TransactionMonitor.cs:58
// initial size
_initialSize = MAX_TRANSACTION_SIZE / MAX_OPEN_TRANSACTIONS;
}
public TransactionService GetTransaction(bool create, bool queryOnly, out bool isNew)
{
var transaction = _slot.Value;
if (create && transaction == null)
{
isNew = true;
bool alreadyLock;
// must lock _transaction before work with _transactions (GetInitialSize use _transactions)
lock (_transactions)
{
if (_transactions.Count >= MAX_OPEN_TRANSACTIONS) throw new LiteException(0, "Maximum number of transactions reached");
var initialSize = this.GetInitialSize();
// check if current thread contains any transaction
alreadyLock = _transactions.Values.Any(x => x.ThreadID == Environment.CurrentManagedThreadId);
transaction = new TransactionService(_header, _locker, _disk, _walIndex, initialSize, this, queryOnly);
// add transaction to execution transaction dict
_transactions[transaction.TransactionID] = transaction;
}
// enter in lock transaction after release _transaction lock
if (alreadyLock == false)
{
try
{
_locker.EnterTransaction();View on GitHub (pinned to f906a5f850)
Solutions
- Ensure every transaction is wrapped in a using/finally so it is committed or disposed even on exceptions (leaked transactions are the most common cause).
- Batch writes into a single shared transaction instead of one transaction per item.
- Reduce concurrency or serialize write workers to stay under 100 in-flight transactions.
- Use the implicit transaction API (collection.Insert) which auto-commits, avoiding manual transaction lifecycle bugs.
Example fix
// before - leaked on exception
var tx = db.BeginTransaction();
doWork(); // throws here
tx.Commit(); // never reached, slot leaks
// after
using (var tx = db.BeginTransaction()) {
doWork();
tx.Commit();
} Defensive patterns
Strategy: retry
Validate before calling
// bound concurrency below the 100-transaction cap
using var sem = new SemaphoreSlim(80);
await Task.WhenAll(jobs.Select(async j => {
await sem.WaitAsync();
try { using var tx = db.BeginTransaction(); /*...*/ tx.Commit(); }
finally { sem.Release(); }
})); Try / catch
try { using var tx = db.BeginTransaction(); /* work */ tx.Commit(); }
catch (LiteException ex) when (ex.Message.Contains("Maximum number of transactions")) {
await Task.Delay(TimeSpan.FromMilliseconds(50));
// retry with backoff
} Prevention
- Always wrap transactions in using/finally to prevent slot leaks.
- Prefer batched single-transaction writes over per-item transactions.
- Throttle parallelism with a SemaphoreSlim well under 100.
When it happens
Trigger: Opening more than 100 simultaneous transactions: heavy multi-threaded workloads, parallel async writes without sharing a transaction, leaked transactions never committed/aborted (each holds a slot until Dispose), or using a new ILiteDatabase connection per thread in a thread-pool storm.
Common situations: Async/await code paths that BeginTransaction but never Commit/Dispose on an exception branch; fan-out parallel tasks each doing its own insert; web request handlers that open transactions without a finally-block disposal.
Related errors
AI-assisted analysis of litedb-org/LiteDB@f906a5f850 (2026-08-13).
Data as JSON: /api/errors/be1c641b72198a39.
Report an issue: GitHub.