microsoft/garnet · error · TsavoriteException

estimatedRecordSize ({estimatedTotalSize}) exceeds max alloc

Error message

estimatedRecordSize ({estimatedTotalSize}) exceeds max allocated heap size (to use: {allocationSizeToUse}; max: {maxHeapAllocationSize})

What it means

DirectCopyInlinePortionOfRecord copies a log record's inline bytes directly into the output SpanByte (network buffer) when it fits; otherwise it allocates heap memory bounded by maxHeapAllocationSize. The throw fires when the record's estimated total size exceeds that budgeted heap allocation, i.e. the record is larger than the per-operation heap memory the caller authorized. For non-object records allocationSizeToUse is estimatedTotalSize+4 (so the check only trips for object-valued records whose stored size exceeds maxHeapAllocationSize).

Source

Thrown at libs/storage/Tsavorite/cs/src/core/Allocator/DiskLogRecord.cs:411

        }

        /// <summary>
        /// Directly copies a record in inline format to the SpanByteAndMemory. Allocates <see cref="SpanByteAndMemory.Memory"/> if needed.
        /// </summary>
        /// <remarks>If <paramref name="output"/>.<see cref="SpanByteAndMemory.IsSpanByte"/>, it points directly to the network buffer so we include the length prefix in the output.</remarks>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static void DirectCopyInlinePortionOfRecord<TSourceLogRecord>(in TSourceLogRecord logRecord, int alignedInlineRecordSize, int estimatedTotalSize, int maxHeapAllocationSize,
            MemoryPool<byte> memoryPool, ref SpanByteAndMemory output)
            where TSourceLogRecord : ISourceLogRecord
        {
            // See if we have enough space in the SpanByte and, if not, if we would fit in maxHeapAllocationSize.
            // For SpanByte the recordSize must include the length prefix, which is included in the output stream
            // if we can write directly to the SpanByte, which is a span in the network buffer.
            if (!output.IsSpanByte || estimatedTotalSize + sizeof(int) > output.SpanByte.Length || logRecord.DataHeader.ValueIsObject)
            {
                var allocationSizeToUse = logRecord.DataHeader.ValueIsObject ? maxHeapAllocationSize : estimatedTotalSize + sizeof(int);
                if (estimatedTotalSize > allocationSizeToUse)
                    throw new TsavoriteException($"estimatedRecordSize ({estimatedTotalSize}) exceeds max allocated heap size (to use: {allocationSizeToUse}; max: {maxHeapAllocationSize})");
                output.EnsureHeapMemorySize(allocationSizeToUse, memoryPool);
            }

            // We must reset the LogRecord's filler size, because we truncated the record down to the (rounded-up) ActualSize if it had been shrunken.
            var newFillerLength = alignedInlineRecordSize - logRecord.ActualSize;
            if (output.IsSpanByte)
            {
                // TotalSize includes the length prefix. If there is a SpanByte it is a span in the network buffer, so we include the prefix length in the output stream.
                var outPtr = output.SpanByte.ToPointer();
                *(int*)outPtr = alignedInlineRecordSize;
                outPtr += sizeof(int);
                Buffer.MemoryCopy((byte*)logRecord.PhysicalAddress, outPtr, alignedInlineRecordSize, alignedInlineRecordSize);
                new LogRecord((long)outPtr).SetFillerLength(newFillerLength);
            }
            else
            {
                // Do not include the length prefix in the output stream; this is done by the caller before writing the stream to the network buffer.
                fixed (byte* outPtr = output.MemorySpan)

View on GitHub (pinned to 951b0fc683)

Solutions

  1. Increase the maxHeapAllocationSize passed to the Serialize/DirectCopy call so it covers your largest record.
  2. Reduce stored value size: chunk or offload large blobs instead of keeping them as single object records.
  3. Use a directly-writable SpanByte output (network buffer) so the direct-copy path is taken and the heap limit is bypassed.
  4. For object values, ensure the configured serializer's max object size matches the data actually being stored.
  5. Log estimatedTotalSize vs maxHeapAllocationSize at write time so oversized records are rejected before they are stored.

Example fix

// before
DiskLogRecord.Serialize(rec, maxHeapAllocationSize: 1 << 20, serializer, pool, ref output);

// after
DiskLogRecord.Serialize(rec, maxHeapAllocationSize: 1 << 24, serializer, pool, ref output);
Defensive patterns

Strategy: validation

Validate before calling

// Reject oversized records before the copy
int est = estimatedTotalSize;
int budget = logRecord.DataHeader.ValueIsObject ? maxHeapAllocationSize : estimatedTotalSize + sizeof(int);
if (est > budget)
    throw new InvalidOperationException($"record {est} > budget {budget}; raise maxHeapAllocationSize or shrink the value");

Try / catch

try { DiskLogRecord.Serialize(rec, maxHeap, ser, pool, ref output); }
catch (TsavoriteException ex) when (ex.Message.Contains("exceeds max allocated heap size"))
{
    // log est vs budget, drop or chunk the record, surface to caller
}

Prevention

When it happens

Trigger: Reading/scanning/serializing a record whose inline value plus length prefix and metadata exceeds maxHeapAllocationSize, while the output is heap-backed (not a directly writable SpanByte) or the record is object-valued. Replication/checkpoint serialization of an oversized object record is the typical path.

Common situations: Large object values in an object store with maxHeapAllocationSize set too low for the workload; deserializing records written under a larger memory budget; checkpoint or replica copy where the per-record heap budget was not scaled to actual value sizes.

Related errors


AI-assisted analysis of microsoft/garnet@951b0fc683 (2026-08-13). Data as JSON: /api/errors/12c4e1e8c6d68402. Report an issue: GitHub.