Difficult to say in general, however, there is many contribution factors.
Partitions are split into segments, and the newest (so-called active) segment in which new data lands is not subject to compaction. Only if new data is written, the active segment would get closed eventually (and a new active segment gets created), making the data available for compaction (there is couple of different configs like segment.size and segment.ms controlling this segment “rolling” process).
IIRC, segment.ms is only checked on-write, ie if you write a tombstone “now” into the active segment, the segment would still not roll even if it’s getting older than segment.ms because only the next write would trigger this check. To you might need to write another message (in you case 4hour later) to trigger the roll.
Also, the log cleaner thread only runs if the dirty-ration is reached, so with low throughput topic, this would build up only slowly. Reducing max.compaction.lag.ms could help to enforce a compaction run base on time, even if dirty ration is not reached yet.