You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I debugged this ELT (a log file with 13+ GB of content), and there are no traces of redundancy, every record appears only once.
The target-postgres batch_processing_time is an average of 20 s to each batch, and it sums to ~6 minutes of total COPY operation time, which is acceptable.
The tap-postgres average extraction time interval between records is ~0.001 s, what leads (roughly) to a total of 90 minutes of data reading.
If anyone has ideas, evidence (or even better, a flame graph 😉) as to what's causing this performance problem, they're more than welcome!
Hi @edgarrmondragon , I've just started looking into this, since I noticed that this variant of tap-postgres is (on my data streams) about 5x slower than the transferwise variant. Unfortunately, I couldn't get viztracer (from the docs) to work when this tap was being run as part of an E+L pipeline via meltano, e.g. viztracer meltano run this-tap some-target. Just double-checking: Would you expect that sort of call to work?
Since so much of this tap's functionality is implemented in the sdk, I started spelunking through code over there. I got as far as def _sync_records()here before stopping -- I can see things that could be slow, but hard to say without a proper tool like viztracer. I'm happy to keep investigating, but could use an assist in actually getting these profiling tools working with my setup. 🙏
Hi @edgarrmondragon , thanks for the pointer! py-spy worked as advertised 🎉
I'm attaching two flamegraphs to this comment, one for a full-refresh run of tap-postgres => target-postgres (both meltanolabs variants), and the other for a log-based incremental run of the same pipeline. As far as I can tell, in both cases, the system quickly gets into an async loop over E+L blocks in ExtractLoadBlocks._start_blocks()here, then spends the vast majority of its time waiting on those blocks to finish in threading.py.
I don't think GitHub lets these SVGs stay interactive, so here's a zoomed-in view of the meltano stack where we're iterating over blocks:
This flamegraph is similar in structure but very different in distribution to the one you linked to in that existing meltano issue. Unfortunately, it doesn't seem like we have great visibility into what's going on, but probably you'll understand what's going on better than I do ☺️
For just a bit more context, here's what the plugin configs look like:
Hi again @edgarrmondragon ! I've been looking back into these perf issues, now that log-based replication state seems to be working as expected. :)
After digging deeper into the code here, and comparing to the related implementation on the transferwise variant -- and assuming I'm understanding it all correctly -- a couple related things jump out at me:
PostgresLogBasedStream.get_records() is called separately for each stream to be replicated, while the other variant's sync_tables() function is called for all streams together
the status_interval of 5s used here is applied per stream, while the 10s used by transferwise is applied for all streams, so the minimum runtime of PostgresLogBasedStream scales as 5s * N streams while for sync_tables it's just 10s
iterating over the db's WAL N times is bound to be slower than iterating just once
I think this ^ is why my changes in PR #667 had such a minor effect on the overall runtime: most of the time was spent repeatedly iterating over the WAL, and not processing the messages that were selected (though I was able to make processing messages faster). Does all this make sense?
Is there any way to run psycopg2's ReplicationCursor.start_replication() over multiple streams at once within the PostgresLogBasedStream framework? Would it make sense to reduce the status_interval to mitigate the minimum runtime scaling by number of streams?
Hi @edgarrmondragon , just a friendly bump. Have you been able to give my previous comment any thought? I'm pretty desperate to migrate off the unmaintained pipelinewise variants, but since postgres E+L is already my performance bottleneck, I can't accept any real degradation here. Would love to help, even if just as a tester.
Is there any way to run psycopg2's ReplicationCursor.start_replication() over multiple streams at once within the PostgresLogBasedStream framework? Would it make sense to reduce the status_interval to mitigate the minimum runtime scaling by number of streams?
Unfortunately not without some non-trivial refactoring here (or much harder, in the SDK). I might be able to take a look in the coming weeks.
Activity
bdewilde commentedon Jul 17, 2025
Hi @edgarrmondragon , I've just started looking into this, since I noticed that this variant of tap-postgres is (on my data streams) about 5x slower than the transferwise variant. Unfortunately, I couldn't get
viztracer(from the docs) to work when this tap was being run as part of an E+L pipeline via meltano, e.g.viztracer meltano run this-tap some-target. Just double-checking: Would you expect that sort of call to work?Since so much of this tap's functionality is implemented in the sdk, I started spelunking through code over there. I got as far as
def _sync_records()here before stopping -- I can see things that could be slow, but hard to say without a proper tool likeviztracer. I'm happy to keep investigating, but could use an assist in actually getting these profiling tools working with my setup. 🙏edgarrmondragon commentedon Jul 18, 2025
Hey @bdewilde, thanks for taking the time to dive into the performance problem 🙏
The way I'd approach this is by running this tap in isolation. That is, clone the repo locally and use viztracer like this:
edgarrmondragon commentedon Jul 18, 2025
I should also mention https://github.com/benfred/py-spy, which we've used in the past to profile Meltano (see meltano/meltano#8342) and some connectors.
bdewilde commentedon Jul 21, 2025
Hi @edgarrmondragon , thanks for the pointer!
py-spyworked as advertised 🎉I'm attaching two flamegraphs to this comment, one for a full-refresh run of tap-postgres => target-postgres (both meltanolabs variants), and the other for a log-based incremental run of the same pipeline. As far as I can tell, in both cases, the system quickly gets into an async loop over E+L blocks in
ExtractLoadBlocks._start_blocks()here, then spends the vast majority of its time waiting on those blocks to finish inthreading.py.I don't think GitHub lets these SVGs stay interactive, so here's a zoomed-in view of the meltano stack where we're iterating over blocks:
This flamegraph is similar in structure but very different in distribution to the one you linked to in that existing meltano issue. Unfortunately, it doesn't seem like we have great visibility into what's going on, but probably you'll understand what's going on better than I do☺️
For just a bit more context, here's what the plugin configs look like:
bdewilde commentedon Jul 21, 2025
Ah, a bit more detail here: When subprocesses are included, you can see the tap/target stacks:
bdewilde commentedon Nov 25, 2025
Hi again @edgarrmondragon ! I've been looking back into these perf issues, now that log-based replication state seems to be working as expected. :)
After digging deeper into the code here, and comparing to the related implementation on the transferwise variant -- and assuming I'm understanding it all correctly -- a couple related things jump out at me:
PostgresLogBasedStream.get_records()is called separately for each stream to be replicated, while the other variant'ssync_tables()function is called for all streams togetherstatus_intervalof 5s used here is applied per stream, while the 10s used by transferwise is applied for all streams, so the minimum runtime ofPostgresLogBasedStreamscales as 5s * N streams while forsync_tablesit's just 10sI think this ^ is why my changes in PR #667 had such a minor effect on the overall runtime: most of the time was spent repeatedly iterating over the WAL, and not processing the messages that were selected (though I was able to make processing messages faster). Does all this make sense?
Is there any way to run psycopg2's
ReplicationCursor.start_replication()over multiple streams at once within thePostgresLogBasedStreamframework? Would it make sense to reduce thestatus_intervalto mitigate the minimum runtime scaling by number of streams?bdewilde commentedon Jan 6, 2026
Hi @edgarrmondragon , just a friendly bump. Have you been able to give my previous comment any thought? I'm pretty desperate to migrate off the unmaintained pipelinewise variants, but since postgres E+L is already my performance bottleneck, I can't accept any real degradation here. Would love to help, even if just as a tester.
edgarrmondragon commentedon Feb 4, 2026
Unfortunately not without some non-trivial refactoring here (or much harder, in the SDK). I might be able to take a look in the coming weeks.
feat: Single-pass WAL streaming for LOG_BASED replication (MeltanoLab…