diff --git a/src/v/compression/tests/CMakeLists.txt b/src/v/compression/tests/CMakeLists.txt
index 15c2da741e35..1011d09a9c72 100644
--- a/src/v/compression/tests/CMakeLists.txt
+++ b/src/v/compression/tests/CMakeLists.txt
@@ -13,3 +13,12 @@ rp_test(
   LABELS compression
   ARGS "-- -c 1"
   )
+
+rp_test(
+  UNIT_TEST
+  BINARY_NAME lz4_buf_tests
+  SOURCES lz4_buf_tests.cc
+  LIBRARIES v::seastar_testing_main v::compression v::rprandom
+  LABELS compression
+  ARGS "-- -c 1"
+  )
diff --git a/src/v/compression/tests/lz4_buf_tests.cc b/src/v/compression/tests/lz4_buf_tests.cc
new file mode 100644
index 000000000000..a27e69aee3ab
--- /dev/null
+++ b/src/v/compression/tests/lz4_buf_tests.cc
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2024 Redpanda Data, Inc.
+ *
+ * Use of this software is governed by the Business Source License
+ * included in the file licenses/BSL.md
+ *
+ * As of the Change Date specified in that file, in accordance with
+ * the Business Source License, use of this software will be governed
+ * by the Apache License, Version 2.0
+ */
+
+#include "compression/internal/lz4_frame_compressor.h"
+#include "compression/lz4_decompression_buffers.h"
+#include "random/generators.h"
+#include "units.h"
+
+#include <seastar/util/defer.hh>
+
+#include <boost/test/unit_test.hpp>
+
+#include <lz4.h>
+
+using enum compression::lz4_decompression_buffers::alloc_ctx::allocation_state;
+
+BOOST_AUTO_TEST_CASE(state_transitions) {
+    auto b = compression::lz4_decompression_buffers{4_MiB, 128_KiB + 1};
+    const auto& buffers = b.buffers();
+    BOOST_REQUIRE_EQUAL(buffers.state, no_buffers_allocated);
+    auto allocator = b.custom_mem_alloc();
+
+    auto* input = allocator.customAlloc(
+      allocator.opaqueState, b.min_alloc_threshold());
+    BOOST_REQUIRE_EQUAL(buffers.state, input_buffer_allocated);
+    BOOST_REQUIRE_EQUAL(input, buffers.input_buffer.get());
+
+    auto* output = allocator.customAlloc(
+      allocator.opaqueState, b.min_alloc_threshold());
+    BOOST_REQUIRE_EQUAL(buffers.state, both_buffers_allocated);
+    BOOST_REQUIRE_EQUAL(output, buffers.output_buffer.get());
+
+    allocator.customFree(allocator.opaqueState, input);
+    BOOST_REQUIRE_EQUAL(buffers.state, output_buffer_allocated);
+
+    allocator.customFree(allocator.opaqueState, output);
+    BOOST_REQUIRE_EQUAL(buffers.state, no_buffers_allocated);
+}
+
+BOOST_AUTO_TEST_CASE(fallback_small_allocs) {
+    auto b = compression::lz4_decompression_buffers{4_MiB, 128_KiB + 1};
+    auto allocator = b.custom_mem_alloc();
+    auto* allocated = allocator.customAlloc(
+      allocator.opaqueState, b.min_alloc_threshold() - 1);
+    BOOST_REQUIRE_NE(allocated, nullptr);
+    allocator.customFree(allocator.opaqueState, allocated);
+}
+
+BOOST_AUTO_TEST_CASE(mixed_allocs) {
+    auto b = compression::lz4_decompression_buffers{4_MiB, 128_KiB + 1};
+    const auto& buffers = b.buffers();
+    auto allocator = b.custom_mem_alloc();
+
+    auto* input = allocator.customAlloc(
+      allocator.opaqueState, b.min_alloc_threshold());
+    BOOST_REQUIRE_EQUAL(input, buffers.input_buffer.get());
+    BOOST_REQUIRE_EQUAL(buffers.state, input_buffer_allocated);
+
+    auto* random_alloc = allocator.customAlloc(
+      allocator.opaqueState, b.min_alloc_threshold() - 1);
+    BOOST_REQUIRE(!buffers.is_managed_address(random_alloc));
+    BOOST_REQUIRE_EQUAL(buffers.state, input_buffer_allocated);
+
+    auto* output = allocator.customAlloc(
+      allocator.opaqueState, b.min_alloc_threshold());
+    BOOST_REQUIRE_EQUAL(output, buffers.output_buffer.get());
+    BOOST_REQUIRE_EQUAL(buffers.state, both_buffers_allocated);
+
+    allocator.customFree(allocator.opaqueState, input);
+    BOOST_REQUIRE_EQUAL(buffers.state, output_buffer_allocated);
+
+    allocator.customFree(allocator.opaqueState, random_alloc);
+    BOOST_REQUIRE_EQUAL(buffers.state, output_buffer_allocated);
+
+    allocator.customFree(allocator.opaqueState, output);
+    BOOST_REQUIRE_EQUAL(buffers.state, no_buffers_allocated);
+}
+
+void test_decompression_calls(
+  compression::lz4_decompression_buffers::stats expected,
+  bool disable_prealloc = false,
+  std::optional<LZ4F_blockSizeID_t> blocksize = std::nullopt) {
+    compression::reset_lz4_decompression_buffers();
+    auto deferred = ss::defer(
+      [] { compression::lz4_decompression_buffers_instance().reset_stats(); });
+
+    if (disable_prealloc) {
+        compression::init_lz4_decompression_buffers(
+          4_MiB, 128_KiB + 1, disable_prealloc);
+    }
+
+    const auto data = random_generators::gen_alphanum_string(512);
+
+    iobuf input;
+    input.append(data.data(), data.size());
+
+    using compression::internal::lz4_frame_compressor;
+    auto& instance = compression::lz4_decompression_buffers_instance();
+    auto compressed = blocksize.has_value()
+                        ? lz4_frame_compressor::compress_with_block_size(
+                          input, blocksize.value())
+                        : lz4_frame_compressor::compress(input);
+    auto uncompressed = lz4_frame_compressor::uncompress(compressed);
+    BOOST_REQUIRE(instance.allocation_stats() == expected);
+}
+
+BOOST_AUTO_TEST_CASE(decompress_large_blocks) {
+    test_decompression_calls(
+      {.allocs = 2,
+       .deallocs = 2,
+       .pass_through_allocs = 1,
+       .pass_through_deallocs = 3},
+      false,
+      LZ4F_max4MB);
+}
+
+BOOST_AUTO_TEST_CASE(decompresss_passthrough_blocks) {
+    test_decompression_calls(
+      {.allocs = 0,
+       .deallocs = 0,
+       .pass_through_allocs = 3,
+       .pass_through_deallocs = 5});
+}
+
+BOOST_AUTO_TEST_CASE(custom_alloc_disabled) {
+    test_decompression_calls(
+      {.allocs = 0,
+       .deallocs = 0,
+       .pass_through_allocs = 0,
+       .pass_through_deallocs = 0},
+      true);
+}