Repository navigation
Avro reader memory leak #2325
Description
Apache Iceberg version
version = "0.9.1"
Please describe the bug 🐞
It seems like there is a memory leak in the avro/reader.py
I have a long running service that keeps crashing. I tried to replicate the issue locally and it seems it also has this issue.
The following code creates a Memory catalog and generates some random data for ingestion into iceberg.
from pyiceberg.catalog.memory import InMemoryCatalog
import tracemalloc
from datetime import datetime, timezone
import polars as pl
def generate_df():
df = pl.DataFrame(
{
"event_type": ["playback"] * 1000,
"event_origin": ["origin1"] * 1000,
"event_send_at": [datetime.now(timezone.utc)] * 1000,
"event_saved_at": [datetime.now(timezone.utc)] * 1000,
"data": [
{
"calendarKey": "calendarKey",
"id": str(i),
"referenceId": f"ref-{i}",
}
for i in range(1000)
],
}
)
return df
df = generate_df()
catalog = InMemoryCatalog("default", warehouse="/tmp/iceberg")
catalog.create_namespace("default")
df = generate_df()
catalog = InMemoryCatalog("default", warehouse="/tmp/iceberg")
catalog.create_namespace("default")
table = iceberg_table = catalog.create_table(
"default.leak", schema=df.to_arrow().schema, location="/tmp/iceberg/leak"
)
df = pl.DataFrame()
tracemalloc.start()
for i in range(1000):
df = generate_df()
df.write_iceberg(table, mode="append")
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics("lineno")
for stat in top_stats[:10]:
print(stat)
Slowly but steadily the outputs for the avro reader memory size increases
/Users/dits/git/play-recommendation-input-consumer/.venv/lib/python3.11/site-packages/pyiceberg/avro/reader.py:330: size=370 KiB, count=3782, average=100 B
/Users/dits/git/play-recommendation-input-consumer/.venv/lib/python3.11/site-packages/pyiceberg/avro/reader.py:190: size=222 KiB, count=1891, average=120 B
/Users/dits/git/play-recommendation-input-consumer/.venv/lib/python3.11/site-packages/pyiceberg/avro/reader.py:133: size=184 KiB, count=5673, average=33 B
After some more writes the output looks like this
/Users/dits/git/play-recommendation-input-consumer/.venv/lib/python3.11/site-packages/pyiceberg/avro/reader.py:330: size=420 KiB, count=4290, average=100 B
/Users/dits/git/play-recommendation-input-consumer/.venv/lib/python3.11/site-packages/pyiceberg/avro/reader.py:190: size=251 KiB, count=2145, average=120 B
/Users/dits/git/play-recommendation-input-consumer/.venv/lib/python3.11/site-packages/pyiceberg/avro/reader.py:133: size=208 KiB, count=6435, average=33 B
If we take a look at the AvroFile class it uses the enter and exit dunder methods. The enter method assigns the reader to a variable on the instance but it seems like the different reader classes sticks around.
https://github.com/apache/iceberg-python/blob/main/pyiceberg/avro/file.py#L192
Willingness to contribute
- I can contribute a fix for this bug independently
- I would be willing to contribute a fix for this bug with guidance from the Iceberg community
- I cannot contribute a fix for this bug at this time
Activity
Declow commentedon Aug 13, 2025
I think I found the root cause.
If we only print the avro/reader
In manifest.py a cache is set https://github.com/apache/iceberg-python/blob/main/pyiceberg/manifest.py#L878
If I set this to 1 the following will always increase but by a very tiny amount 200 byte per write.
The higher the max cache value the faster the memory increases.
If I remove the cache altogether the memory will be released correctly no matter the amount of iterations.
sungwy commentedon Aug 15, 2025
Hi @Declow — thanks for reporting this issue. If your observation is correct, it’s definitely a serious matter, and we should address it promptly. I appreciate your initiative in digging into the root cause.
Even with the cache set to
1, it still stores a tuple ofManifestFileobjects, which grows with each new commit. Since eachManifestFilehas the base overhead of a Python object instance plus some attributes (e.g., file path, partition), an increase of around ~100 B to ~500 B per commit seems reasonable.Did you notice any memory usage growth beyond this range that might indicate a more substantial leak?
Declow commentedon Aug 15, 2025
For my toy example here it was not that bad. When I used our production setup with a lot more manifest files the service went from using 800MB RAM to reaching it max of 1556MB and crashing within 5 minutes. It was using almost 10MB for each commit maybe more.
When I fixed the issue it on the service by removing the cache everything was stable.
sungwy commentedon Aug 15, 2025
I'm wondering if that just maybe the size of a large manifest list * 128. How many manifests are you materializing in a single manifest-list path? Do you have an estimate of the memory used for a single return of
_manifests, i.e.tuple(read_manifest_list(file))for your Iceberg table?kevinjqliu commentedon Aug 16, 2025
Thanks for creating the issue. The repro script is super helpful.
This is my current hypothesis:
I think what you're observing is the side affect of the write pattern and its interaction with the
_manifestsLRU cache.appendthe dataframe here will always create a new manifest entry since we use the "fast append" strategy.This means each write will generate a new manifest list.
_manifests, with its LRU cache, will cache the new manifest list's path along with the tuple ofManifestFileobjects.Each cached manifest list path will have overlapping
ManifestFileobjects since we're constantly adding new manifest files through append.This means in a LRU cache of size 128, we cache 128 manifest file paths. the first cached item will have 1 tuple of
ManifestFile, and the 128th cached item will have 128 tuples ofManifestFile. Most of theseManifestFileare referring to the same underlying manifest file but in a python new object.Each of these
ManifestFileobjects will refer to the data read using the avro reader.This is the top allocation, even above the avro reader objects. I believe these are just
ManifestFileobjectsHeres a modified repro script:
Let me know if this makes sense. I'd like to hear what others think
Declow commentedon Aug 18, 2025
@kevinjqliu It makes sense that the list of manifest files keeps increasing as the write continues but it is not root cause.
As long as the function is cached the AvroFileHeader is never GC'ed. These objects are created each time a AvroFile is read inside of read_manifest_list.
If you run the script with any cache size it will continue to increase the memory usage.
/avro/reader.py:330: size=116 KiB, count=1190, average=100 B
If you run the script for more than 50 iterations.
kris-gaudel commentedon Aug 21, 2025
Flame graph and memory graph after running the script with 50 iterations
kris-gaudel commentedon Aug 21, 2025
After 100 iterations
andormarkus commentedon Aug 22, 2025
We're experiencing this exact same memory leak in production AWS Lambda functions using pyiceberg. Our data shows the same avro/reader.py line growth:
Memory progression across Lambda invocations (same 50MB file):
reader.py:330: 52.8 MiBreader.py:330: 88.0 MiBreader.py:330: 106 MiBMemory grows from 1.4GB to 2GB+ after just 5 invocations processing identical files. Despite aggressive cleanup attempts with
gc.collect()andlibc.malloc_trim(0), the avro reader allocations persist between Lambda executions.Our logs confirm @Declow's findings. This is causing production Lambda functions to hit memory limits and crash.
Happy to test any proposed fixes in our production environment with concurrent Lambda executions.
kevinjqliu commentedon Aug 22, 2025
thanks for all the great discussion here. I want to also mention #2004, where we're looking to replace the cython avro reader with one from iceberg-rust. this might help but i think the main issue is still with the cache.
this is also what im seeing. but it looks like it we manually clear the cache, gc will clean up the objects
@kris-gaudel 's graph shows that at least this is bounded... :)
@andormarkus could you try changing the cache size or manually clear the cache here?
iceberg-python/pyiceberg/manifest.py
Line 878 in 19efd2d
andormarkus commentedon Aug 22, 2025
I have implemented manual clearing of the
_manifestscache but the memory leak persists in AWS Lambda.Environment: AWS Lambda, PyIceberg with Glue catalog
Cache Clearing Implementation:
Results: Memory still grows from 1438MB → 1858MB across invocations. The manifest cache clearing had minimal impact.
Declow commentedon Aug 22, 2025
Well there is a trend up. This is after 300. It does not seem bounded.
@andormarkus Clearing the cache does help
But even if I remove the cache completely it still increases. The caching just makes it way worse.
kris-gaudel commentedon Aug 23, 2025
I was running memray on 1000 iterations and the usage increases over time significantly (detailed via the logs). I couldn't generate a flame graph bc of the binary size being too large
iceberg-python/pyiceberg/avro/file.py
Line 192 in a33cd38
12 remaining items
thomas-pfeiffer commentedon Dec 3, 2025
We ran into this issue (like @andormarkus' case) on AWS Lambda as well. We tried the above mentioned option with regularly clearing the cache but the results were similar with the test script and the memory usage in the real Lambda was still too high for our scenario, hence we opted to deactivated the cache completely:
(reg.
__wrapped__see https://cachetools.readthedocs.io/en/latest/#cachetools.cached)So it's only a workaround and probably we leave some performance on the table, but we valued the stability and less retried Lambda executions above pure performance. Your milage may vary.
andormarkus commentedon Dec 8, 2025
Hi @thomas-pfeiffer,
Thanks for sharing your workaround! I'm curious about your specific scenario that led you to disable the cache completely.
In my case, I implemented cache clearing at invocation boundaries:
This approach gives me:
With this strategy, my Lambda memory stabilizes at 1600-1800MB instead of continuously growing to OOM, while still maintaining cache performance benefits within each execution. I'm running with 2048MB memory allocation, so this 10-20% overhead is acceptable in our environment.
Could you share more details about your scenario?
Understanding your use case would help the community find the right balance between performance and memory stability.
Thanks!
Declow commentedon Dec 8, 2025
@andormarkus
I ended up disabling the cache completely as well.
It depends on your use-case if this is really true.
My use-case is I consume a stream of events and append the data to iceberg.
The way that the cache is right now does not give additional performance.
When data is appended to iceberg it creates a key in the cache like so.
[key1] -> list of manifests [manifest1]
A second append
[key2] -> list of manifests [manifest1, manifest2]
The first key is never activated and the cache is just updated with a new key. A lot of the same manifest files will just be referenced multiple times in the cache. This is why the overall memory increases so rapidly.
So if your use-case is just writing data to iceberg then you might as well disable the cache.
thomas-pfeiffer commentedon Dec 8, 2025
@andormarkus reg. your questions:
I use the repro script from #2325 (comment) and when I disabled the cache completely, the memory was fully stable even after 2.000 executions. I guess the stability was much more a concern since our individual Lambda executions run 2-3 mins, hence repeating failed executions caused by out of memory errors are worse for us then losing a few seconds on a not-cached manifest. (Not taking the remark from #2325 (comment) into account.)
Not really. Disabling the cache was the simpler solution. And since we leverage some other things we felt eradicating this memory leak source completely seemed the better choice for us.
Average duration was between 2-3 min in the last days; Depends a bit on the incoming data in our use case. Haven't checked regarding the manifest list tbh.
We allocated
2048MBto the AWS Lambda. On some days the usage goes up to2022MB, but it's not very consistent e.g., yesterday it was mostly around~1400MB. Depends highly on the incoming data for us, but no out of memory errors since the workaround so far.Small remark / question reg. your approach:
@andormarkus Doesn't that mean you clean your cache twice immediately one another? I would have assumed the post-execution cache cleaning to be enough.
Our use case seems similar to @Declow scenario, where we do only appends on the tables we write. I guess we would have no hits on the cache. (Haven't tried it tbh.)
Maybe 2 somewhat stupid questions for the round here:
kevinjqliu commentedon Jan 25, 2026
Hey all, thanks for all the detailed discussion here.
Summarizing from what I've read above
I've opened #2951 to cache the ManifestFile
We should fix the avro reader leak as well
Its probably a good idea to use the script above and create a benchmark script to test for memory leaks
kevinjqliu commentedon Jan 25, 2026
im happy with the current state of #2951
Please give it a try.
We should also make the cache size configurable, and be able to disable it too.
kris-gaudel commentedon Jan 25, 2026
Thanks for the follow-up @kevinjqliu, is there an issue to make the cache size configurable/choice to disable it? I'd love to work on that
kevinjqliu commentedon Jan 25, 2026
Thanks for creating the issue! 😄
fix manifest cache (#2951)
kevinjqliu commentedon Jan 29, 2026
#2951 is now merged. I tried to benchmark the memory leak in Avro reader, but found most of the memory usage was caused by the cache. I'm curious if the Avro reader issue is still significant after this fix.
Make manifest cache size configurable (#2993)
github-actions commentedon Jul 29, 2026
This issue has been automatically marked as stale because it has been open for 180 days with no activity. It will be closed in next 14 days if no further activity occurs. To permanently prevent this issue from being considered stale, add the label 'not-stale', but commenting on the issue is preferred when possible.
github-actions commentedon Aug 13, 2026
This issue has been closed because it has not received any activity in the last 14 days since being marked as 'stale'