diff --git a/src/blockchain_db/blockchain_db.cpp b/src/blockchain_db/blockchain_db.cpp
index 5fec22d8d..1a6a19da5 100644
--- a/src/blockchain_db/blockchain_db.cpp
+++ b/src/blockchain_db/blockchain_db.cpp
@@ -67,7 +67,7 @@ bool matches_category(relay_method method, relay_category category) noexcept
     case relay_method::local:
       return false;
     case relay_method::block:
-    case relay_method::flood:
+    case relay_method::fluff:
       return true;
     case relay_method::none:
       break;
@@ -90,7 +90,7 @@ void txpool_tx_meta_t::set_relay_method(relay_method method) noexcept
       is_local = 1;
       break;
     default:
-    case relay_method::flood:
+    case relay_method::fluff:
       break;
     case relay_method::block:
       kept_by_block = 1;
@@ -106,7 +106,7 @@ relay_method txpool_tx_meta_t::get_relay_method() const noexcept
     return relay_method::none;
   if (is_local)
     return relay_method::local;
-  return relay_method::flood;
+  return relay_method::fluff;
 }
 
 const command_line::arg_descriptor<std::string> arg_db_sync_mode = {
diff --git a/src/blockchain_db/blockchain_db.h b/src/blockchain_db/blockchain_db.h
index b2c5d6cb4..acd7976a8 100644
--- a/src/blockchain_db/blockchain_db.h
+++ b/src/blockchain_db/blockchain_db.h
@@ -108,7 +108,7 @@ extern const command_line::arg_descriptor<bool, false> arg_db_salvage;
 
 enum class relay_category : uint8_t
 {
-  broadcasted = 0,//!< Public txes received via block/flooding/fluff
+  broadcasted = 0,//!< Public txes received via block/fluff
   relayable,      //!< Every tx not marked `relay_method::none`
   legacy,         //!< `relay_category::broadcasted` + `relay_method::none` for rpc relay requests or historical reasons
   all             //!< Everything in the db
diff --git a/src/cryptonote_config.h b/src/cryptonote_config.h
index ca127c3ee..134b630f7 100644
--- a/src/cryptonote_config.h
+++ b/src/cryptonote_config.h
@@ -101,6 +101,9 @@
 #define CRYPTONOTE_MEMPOOL_TX_LIVETIME                    (86400*3) //seconds, three days
 #define CRYPTONOTE_MEMPOOL_TX_FROM_ALT_BLOCK_LIVETIME     604800 //seconds, one week
 
+
+#define CRYPTONOTE_DANDELIONPP_FLUSH_AVERAGE 5 // seconds
+
 // see src/cryptonote_protocol/levin_notify.cpp
 #define CRYPTONOTE_NOISE_MIN_EPOCH                      5      // minutes
 #define CRYPTONOTE_NOISE_EPOCH_RANGE                    30     // seconds
diff --git a/src/cryptonote_core/cryptonote_core.cpp b/src/cryptonote_core/cryptonote_core.cpp
index cf23a652c..23f13000c 100644
--- a/src/cryptonote_core/cryptonote_core.cpp
+++ b/src/cryptonote_core/cryptonote_core.cpp
@@ -174,11 +174,6 @@ namespace cryptonote
   , "Relay blocks as normal blocks"
   , false
   };
-  static const command_line::arg_descriptor<bool> arg_pad_transactions  = {
-    "pad-transactions"
-  , "Pad relayed transactions to help defend against traffic volume analysis"
-  , false
-  };
   static const command_line::arg_descriptor<size_t> arg_max_txpool_weight  = {
     "max-txpool-weight"
   , "Set maximum txpool weight in bytes."
@@ -235,8 +230,7 @@ namespace cryptonote
               m_disable_dns_checkpoints(false),
               m_update_download(0),
               m_nettype(UNDEFINED),
-              m_update_available(false),
-              m_pad_transactions(false)
+              m_update_available(false)
   {
     m_checkpoints_updating.clear();
     set_cryptonote_protocol(pprotocol);
@@ -333,7 +327,6 @@ namespace cryptonote
     command_line::add_arg(desc, arg_block_download_max_size);
     command_line::add_arg(desc, arg_sync_pruned_blocks);
     command_line::add_arg(desc, arg_max_txpool_weight);
-    command_line::add_arg(desc, arg_pad_transactions);
     command_line::add_arg(desc, arg_block_notify);
     command_line::add_arg(desc, arg_prune_blockchain);
     command_line::add_arg(desc, arg_reorg_notify);
@@ -376,7 +369,6 @@ namespace cryptonote
     set_enforce_dns_checkpoints(command_line::get_arg(vm, arg_dns_checkpoints));
     test_drop_download_height(command_line::get_arg(vm, arg_test_drop_download_height));
     m_fluffy_blocks_enabled = !get_arg(vm, arg_no_fluffy_blocks);
-    m_pad_transactions = get_arg(vm, arg_pad_transactions);
     m_offline = get_arg(vm, arg_offline);
     m_disable_dns_checkpoints = get_arg(vm, arg_disable_dns_checkpoints);
     if (!command_line::is_arg_defaulted(vm, arg_fluffy_blocks))
@@ -1295,7 +1287,7 @@ namespace cryptonote
             private_req.txs.push_back(std::move(std::get<1>(tx)));
             break;
           case relay_method::block:
-          case relay_method::flood:
+          case relay_method::fluff:
             public_req.txs.push_back(std::move(std::get<1>(tx)));
             break;
         }
diff --git a/src/cryptonote_core/cryptonote_core.h b/src/cryptonote_core/cryptonote_core.h
index 4b67984ab..55efb566c 100644
--- a/src/cryptonote_core/cryptonote_core.h
+++ b/src/cryptonote_core/cryptonote_core.h
@@ -791,13 +791,6 @@ namespace cryptonote
       */
      bool fluffy_blocks_enabled() const { return m_fluffy_blocks_enabled; }
 
-     /**
-      * @brief get whether transaction relay should be padded
-      *
-      * @return whether transaction relay should be padded
-      */
-     bool pad_transactions() const { return m_pad_transactions; }
-
      /**
       * @brief check a set of hashes against the precompiled hash set
       *
@@ -1102,7 +1095,6 @@ namespace cryptonote
 
      bool m_fluffy_blocks_enabled;
      bool m_offline;
-     bool m_pad_transactions;
 
      std::shared_ptr<tools::Notify> m_block_rate_notify;
    };
diff --git a/src/cryptonote_protocol/cryptonote_protocol_handler.inl b/src/cryptonote_protocol/cryptonote_protocol_handler.inl
index e20934a25..ae4abeef5 100644
--- a/src/cryptonote_protocol/cryptonote_protocol_handler.inl
+++ b/src/cryptonote_protocol/cryptonote_protocol_handler.inl
@@ -910,7 +910,7 @@ namespace cryptonote
     for (size_t i = 0; i < arg.txs.size(); ++i)
     {
       cryptonote::tx_verification_context tvc{};
-      m_core.handle_incoming_tx({arg.txs[i], crypto::null_hash}, tvc, relay_method::flood, true);
+      m_core.handle_incoming_tx({arg.txs[i], crypto::null_hash}, tvc, relay_method::fluff, true);
       if(tvc.m_verifivation_failed)
       {
         LOG_PRINT_CCONTEXT_L1("Tx verification failed, dropping connection");
@@ -2351,7 +2351,7 @@ skip:
        local mempool before doing the relay. The code was already updating the
        DB twice on received transactions - it is difficult to workaround this
        due to the internal design. */
-    return m_p2p->send_txs(std::move(arg.txs), zone, source, m_core, m_core.pad_transactions()) != epee::net_utils::zone::invalid;
+    return m_p2p->send_txs(std::move(arg.txs), zone, source, m_core) != epee::net_utils::zone::invalid;
   }
   //------------------------------------------------------------------------------------------------------------------------
   template<class t_core>
diff --git a/src/cryptonote_protocol/enums.h b/src/cryptonote_protocol/enums.h
index ad4eedf4c..2ec622d94 100644
--- a/src/cryptonote_protocol/enums.h
+++ b/src/cryptonote_protocol/enums.h
@@ -38,6 +38,6 @@ namespace cryptonote
     none = 0, //!< Received via RPC with `do_not_relay` set
     local,    //!< Received via RPC; trying to send over i2p/tor, etc.
     block,    //!< Received in block, takes precedence over others
-    flood     //!< Received/sent over public networks
+    fluff     //!< Received/sent over public networks
   };
 }
diff --git a/src/cryptonote_protocol/levin_notify.cpp b/src/cryptonote_protocol/levin_notify.cpp
index 4b41b5bfc..a0a4bbbb1 100644
--- a/src/cryptonote_protocol/levin_notify.cpp
+++ b/src/cryptonote_protocol/levin_notify.cpp
@@ -33,6 +33,7 @@
 #include <chrono>
 #include <deque>
 #include <stdexcept>
+#include <utility>
 
 #include "common/expect.h"
 #include "common/varint.h"
@@ -57,6 +58,37 @@ namespace levin
     constexpr const std::chrono::seconds noise_min_delay{CRYPTONOTE_NOISE_MIN_DELAY};
     constexpr const std::chrono::seconds noise_delay_range{CRYPTONOTE_NOISE_DELAY_RANGE};
 
+    /* A custom duration is used for the poisson distribution because of the
+       variance. If 5 seconds is given to `std::poisson_distribution`, 95% of
+       the values fall between 1-9s in 1s increments (not granular enough). If
+       5000 milliseconds is given, 95% of the values fall between 4859ms-5141ms
+       in 1ms increments (not enough time variance). Providing 20 quarter
+       seconds yields 95% of the values between 3s-7.25s in 1/4s increments. */
+    using fluff_stepsize = std::chrono::duration<std::chrono::milliseconds::rep, std::ratio<1, 4>>;
+    constexpr const std::chrono::seconds fluff_average_in{CRYPTONOTE_DANDELIONPP_FLUSH_AVERAGE};
+
+    /*! Bitcoin Core is using 1/2 average seconds for outgoing connections
+        compared to incoming. The thinking is that the user controls outgoing
+        connections (Dandelion++ makes similar assumptions in its stem
+        algorithm). The randomization yields 95% values between 1s-4s in
+	1/4s increments. */
+    constexpr const fluff_stepsize fluff_average_out{fluff_stepsize{fluff_average_in} / 2};
+
+    class random_poisson
+    {
+      std::poisson_distribution<fluff_stepsize::rep> dist;
+    public:
+      explicit random_poisson(fluff_stepsize average)
+        : dist(average.count() < 0 ? 0 : average.count())
+      {}
+
+      fluff_stepsize operator()()
+      {
+        crypto::random_device rand{};
+        return fluff_stepsize{dist(rand)};
+      }
+    };
+
     /*! Select a randomized duration from 0 to `range`. The precision will be to
         the systems `steady_clock`. As an example, supplying 3 seconds to this
         function will select a duration from [0, 3] seconds, and the increments
@@ -129,6 +161,12 @@ namespace levin
       return fullBlob;
     }
 
+    bool make_payload_send_txs(connections& p2p, std::vector<blobdata>&& txs, const boost::uuids::uuid& destination, const bool pad)
+    {
+      const cryptonote::blobdata blob = make_tx_payload(std::move(txs), pad);
+      return p2p.notify(NOTIFY_NEW_TRANSACTIONS::ID, epee::strspan<std::uint8_t>(blob), destination);
+    }
+
     /* The current design uses `asio::strand`s. The documentation isn't as clear
        as it should be - a `strand` has an internal `mutex` and `bool`. The
        `mutex` synchronizes thread access and the `bool` is set when a thread is
@@ -187,15 +225,18 @@ namespace levin
   {
     struct zone
     {
-      explicit zone(boost::asio::io_service& io_service, std::shared_ptr<connections> p2p, epee::byte_slice noise_in, bool is_public)
+      explicit zone(boost::asio::io_service& io_service, std::shared_ptr<connections> p2p, epee::byte_slice noise_in, bool is_public, bool pad_txs)
         : p2p(std::move(p2p)),
           noise(std::move(noise_in)),
           next_epoch(io_service),
+          flush_txs(io_service),
           strand(io_service),
           map(),
           channels(),
+          flush_time(std::chrono::steady_clock::time_point::max()),
           connection_count(0),
-          is_public(is_public)
+          is_public(is_public),
+          pad_txs(pad_txs)
       {
         for (std::size_t count = 0; !noise.empty() && count < CRYPTONOTE_NOISE_CHANNELS; ++count)
           channels.emplace_back(io_service);
@@ -204,11 +245,14 @@ namespace levin
       const std::shared_ptr<connections> p2p;
       const epee::byte_slice noise; //!< `!empty()` means zone is using noise channels
       boost::asio::steady_timer next_epoch;
+      boost::asio::steady_timer flush_txs;
       boost::asio::io_service::strand strand;
       net::dandelionpp::connection_map map;//!< Tracks outgoing uuid's for noise channels or Dandelion++ stems
       std::deque<noise_channel> channels;  //!< Never touch after init; only update elements on `noise_channel.strand`
+      std::chrono::steady_clock::time_point flush_time; //!< Next expected Dandelion++ fluff flush
       std::atomic<std::size_t> connection_count; //!< Only update in strand, can be read at any time
       const bool is_public;                      //!< Zone is public ipv4/ipv6 connections
+      const bool pad_txs;                        //!< Pad txs to the next boundary for privacy
     };
   } // detail
 
@@ -245,49 +289,112 @@ namespace levin
       }
     };
 
-    //! Sends a message to every active connection
-    class flood_notify
+    //! Sends txs on connections with expired timers, and queues callback for next timer expiration (if any).
+    struct fluff_flush
     {
       std::shared_ptr<detail::zone> zone_;
-      epee::byte_slice message_; // Requires manual copy
-      boost::uuids::uuid source_;
+      std::chrono::steady_clock::time_point flush_time_;
 
-    public:
-      explicit flood_notify(std::shared_ptr<detail::zone> zone, epee::byte_slice message, const boost::uuids::uuid& source)
-        : zone_(std::move(zone)), message_(message.clone()), source_(source)
-      {}
+      static void queue(std::shared_ptr<detail::zone> zone, const std::chrono::steady_clock::time_point flush_time)
+      {
+        assert(zone != nullptr);
+        assert(zone->strand.running_in_this_thread());
 
-      flood_notify(flood_notify&&) = default;
-      flood_notify(const flood_notify& source)
-        : zone_(source.zone_), message_(source.message_.clone()), source_(source.source_)
-      {}
+        detail::zone& this_zone = *zone;
+        this_zone.flush_time = flush_time;
+        this_zone.flush_txs.expires_at(flush_time);
+        this_zone.flush_txs.async_wait(this_zone.strand.wrap(fluff_flush{std::move(zone), flush_time}));
+      }
 
-      void operator()() const
+      void operator()(const boost::system::error_code error)
       {
         if (!zone_ || !zone_->p2p)
           return;
 
         assert(zone_->strand.running_in_this_thread());
 
-        /* The foreach should be quick, but then it iterates and acquires the
-           same lock for every connection. So do in a strand because two threads
-           will ping-pong each other with cacheline invalidations. Revisit if
-           algorithm changes or the locking strategy within the levin config
-           class changes. */
+        const bool timer_error = bool(error);
+        if (timer_error)
+        {
+          if (error != boost::system::errc::operation_canceled)
+            throw boost::system::system_error{error, "fluff_flush timer failed"};
 
-        std::vector<boost::uuids::uuid> connections;
-        connections.reserve(connection_id_reserve_size);
-        zone_->p2p->foreach_connection([this, &connections] (detail::p2p_context& context) {
-          /* Only send to outgoing connections when "flooding" over i2p/tor.
-             Otherwise this makes the tx linkable to a hidden service address,
-             making things linkable across connections. */
-          if (this->source_ != context.m_connection_id && (this->zone_->is_public || !context.m_is_income))
-            connections.emplace_back(context.m_connection_id);
+          // new timer canceled this one set in future
+          if (zone_->flush_time < flush_time_)
+            return;
+        }
+
+        const auto now = std::chrono::steady_clock::now();
+        auto next_flush = std::chrono::steady_clock::time_point::max();
+        std::vector<std::pair<std::vector<blobdata>, boost::uuids::uuid>> connections{};
+        zone_->p2p->foreach_connection([timer_error, now, &next_flush, &connections] (detail::p2p_context& context)
+        {
+          if (!context.fluff_txs.empty())
+          {
+            if (context.flush_time <= now || timer_error) // flush on canceled timer
+            {
+              context.flush_time = std::chrono::steady_clock::time_point::max();
+              connections.emplace_back(std::move(context.fluff_txs), context.m_connection_id);
+              context.fluff_txs.clear();
+            }
+            else // not flushing yet
+              next_flush = std::min(next_flush, context.flush_time);
+          }
+          else // nothing to flush
+            context.flush_time = std::chrono::steady_clock::time_point::max();
           return true;
         });
 
-        for (const boost::uuids::uuid& connection : connections)
-          zone_->p2p->send(message_.clone(), connection);
+        for (auto& connection : connections)
+          make_payload_send_txs(*zone_->p2p, std::move(connection.first), connection.second, zone_->pad_txs);
+
+        if (next_flush != std::chrono::steady_clock::time_point::max())
+          fluff_flush::queue(std::move(zone_), next_flush);
+        else
+          zone_->flush_time = next_flush; // signal that no timer is set
+      }
+    };
+
+    /*! The "fluff" portion of the Dandelion++ algorithm. Every tx is queued
+        per-connection and flushed with a randomized poisson timer. This
+        implementation only has one system timer per-zone, and instead tracks
+        the lowest flush time. */
+    struct fluff_notify
+    {
+      std::shared_ptr<detail::zone> zone_;
+      std::vector<blobdata> txs_;
+      boost::uuids::uuid source_;
+
+      void operator()()
+      {
+        if (!zone_ || !zone_->p2p || txs_.empty())
+          return;
+
+        assert(zone_->strand.running_in_this_thread());
+
+        const auto now = std::chrono::steady_clock::now();
+        auto next_flush = std::chrono::steady_clock::time_point::max();
+
+        random_poisson in_duration(fluff_average_in);
+        random_poisson out_duration(fluff_average_out);
+
+        zone_->p2p->foreach_connection([this, now, &in_duration, &out_duration, &next_flush] (detail::p2p_context& context)
+        {
+          if (this->source_ != context.m_connection_id && (this->zone_->is_public || !context.m_is_income))
+          {
+            if (context.fluff_txs.empty())
+              context.flush_time = now + (context.m_is_income ? in_duration() : out_duration());
+
+            next_flush = std::min(next_flush, context.flush_time);
+            context.fluff_txs.reserve(context.fluff_txs.size() + this->txs_.size());
+            for (const blobdata& tx : this->txs_)
+              context.fluff_txs.push_back(tx); // must copy instead of move (multiple conns)
+          }
+          return true;
+        });
+
+        if (next_flush < zone_->flush_time)
+          fluff_flush::queue(std::move(zone_), next_flush);
       }
     };
 
@@ -451,7 +558,7 @@ namespace levin
       }
     };
 
-    //! Prepares connections for new channel epoch and sets timer for next epoch
+    //! Prepares connections for new channel/dandelionpp epoch and sets timer for next epoch
     struct start_epoch
     {
       // Variables allow for Dandelion++ extension
@@ -481,8 +588,8 @@ namespace levin
     };
   } // anonymous
 
-  notify::notify(boost::asio::io_service& service, std::shared_ptr<connections> p2p, epee::byte_slice noise, bool is_public)
-    : zone_(std::make_shared<detail::zone>(service, std::move(p2p), std::move(noise), is_public))
+  notify::notify(boost::asio::io_service& service, std::shared_ptr<connections> p2p, epee::byte_slice noise, const bool is_public, const bool pad_txs)
+    : zone_(std::make_shared<detail::zone>(service, std::move(p2p), std::move(noise), is_public, pad_txs))
   {
     if (!zone_->p2p)
       throw std::logic_error{"cryptonote::levin::notify cannot have nullptr p2p argument"};
@@ -533,8 +640,18 @@ namespace levin
       channel.next_noise.cancel();
   }
 
-  bool notify::send_txs(std::vector<blobdata> txs, const boost::uuids::uuid& source, const bool pad_txs)
+  void notify::run_fluff()
   {
+    if (!zone_)
+      return;
+    zone_->flush_txs.cancel();
+  }
+
+  bool notify::send_txs(std::vector<blobdata> txs, const boost::uuids::uuid& source)
+  {
+    if (txs.empty())
+      return true;
+
     if (!zone_)
       return false;
 
@@ -565,12 +682,7 @@ namespace levin
     }
     else
     {
-      const std::string payload = make_tx_payload(std::move(txs), pad_txs);
-      epee::byte_slice message =
-        epee::levin::make_notify(NOTIFY_NEW_TRANSACTIONS::ID, epee::strspan<std::uint8_t>(payload));
-
-      // traditional monero send technique
-      zone_->strand.dispatch(flood_notify{zone_, std::move(message), source});
+      zone_->strand.dispatch(fluff_notify{zone_, std::move(txs), source});
     }
 
     return true;
diff --git a/src/cryptonote_protocol/levin_notify.h b/src/cryptonote_protocol/levin_notify.h
index 8bc9b72fa..ce652d933 100644
--- a/src/cryptonote_protocol/levin_notify.h
+++ b/src/cryptonote_protocol/levin_notify.h
@@ -82,7 +82,7 @@ namespace levin
     {}
 
     //! Construct an instance with available notification `zones`.
-    explicit notify(boost::asio::io_service& service, std::shared_ptr<connections> p2p, epee::byte_slice noise, bool is_public);
+    explicit notify(boost::asio::io_service& service, std::shared_ptr<connections> p2p, epee::byte_slice noise, bool is_public, bool pad_txs);
 
     notify(const notify&) = delete;
     notify(notify&&) = default;
@@ -104,11 +104,14 @@ namespace levin
     //! Run the logic for the next stem timeout imemdiately. Only use in  testing.
     void run_stems();
 
+    //! Run the logic for flushing all Dandelion++ fluff queued txs. Only use in testing.
+    void run_fluff();
+
     /*! Send txs using `cryptonote_protocol_defs.h` payload format wrapped in a
         levin header. The message will be sent in a "discreet" manner if
         enabled - if `!noise.empty()` then the `command`/`payload` will be
         queued to send at the next available noise interval. Otherwise, a
-        standard Monero flood notification will be used.
+        Dandelion++ fluff algorithm will be used.
 
         \note Eventually Dandelion++ stem sending will be used here when
           enabled.
@@ -117,12 +120,9 @@ namespace levin
         \param source The source of the notification. `is_nil()` indicates this
           node is the source. Dandelion++ will use this to map a source to a
           particular stem.
-        \param pad_txs A request to pad txs to help conceal origin via
-          statistical analysis. Ignored if noise was enabled during
-          construction.
 
       \return True iff the notification is queued for sending. */
-    bool send_txs(std::vector<blobdata> txs, const boost::uuids::uuid& source, bool pad_txs);
+    bool send_txs(std::vector<blobdata> txs, const boost::uuids::uuid& source);
   };
 } // levin
 } // net
diff --git a/src/p2p/net_node.cpp b/src/p2p/net_node.cpp
index 58c1717e0..58c5706de 100644
--- a/src/p2p/net_node.cpp
+++ b/src/p2p/net_node.cpp
@@ -161,6 +161,10 @@ namespace nodetool
     const command_line::arg_descriptor<int64_t> arg_limit_rate_down = {"limit-rate-down", "set limit-rate-down [kB/s]", P2P_DEFAULT_LIMIT_RATE_DOWN};
     const command_line::arg_descriptor<int64_t> arg_limit_rate = {"limit-rate", "set limit-rate [kB/s]", -1};
 
+    const command_line::arg_descriptor<bool> arg_pad_transactions = {
+      "pad-transactions", "Pad relayed transactions to help defend against traffic volume analysis", false
+    };
+
     boost::optional<std::vector<proxy>> get_proxies(boost::program_options::variables_map const& vm)
     {
         namespace ip = boost::asio::ip;
diff --git a/src/p2p/net_node.h b/src/p2p/net_node.h
index ee70c2806..0e9c1c942 100644
--- a/src/p2p/net_node.h
+++ b/src/p2p/net_node.h
@@ -38,6 +38,7 @@
 #include <boost/program_options/options_description.hpp>
 #include <boost/program_options/variables_map.hpp>
 #include <boost/uuid/uuid.hpp>
+#include <chrono>
 #include <functional>
 #include <utility>
 #include <vector>
@@ -109,8 +110,16 @@ namespace nodetool
   template<class base_type>
   struct p2p_connection_context_t: base_type //t_payload_net_handler::connection_context //public net_utils::connection_context_base
   {
-    p2p_connection_context_t(): peer_id(0), support_flags(0), m_in_timedsync(false) {}
+    p2p_connection_context_t()
+      : fluff_txs(),
+        flush_time(std::chrono::steady_clock::time_point::max()),
+        peer_id(0),
+        support_flags(0),
+        m_in_timedsync(false)
+    {}
 
+    std::vector<cryptonote::blobdata> fluff_txs;
+    std::chrono::steady_clock::time_point flush_time;
     peerid_type peer_id;
     uint32_t support_flags;
     bool m_in_timedsync;
@@ -337,7 +346,7 @@ namespace nodetool
     virtual void callback(p2p_connection_context& context);
     //----------------- i_p2p_endpoint -------------------------------------------------------------
     virtual bool relay_notify_to_list(int command, const epee::span<const uint8_t> data_buff, std::vector<std::pair<epee::net_utils::zone, boost::uuids::uuid>> connections);
-    virtual epee::net_utils::zone send_txs(std::vector<cryptonote::blobdata> txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, cryptonote::i_core_events& core, bool pad_txs);
+    virtual epee::net_utils::zone send_txs(std::vector<cryptonote::blobdata> txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, cryptonote::i_core_events& core);
     virtual bool invoke_command_to_peer(int command, const epee::span<const uint8_t> req_buff, std::string& resp_buff, const epee::net_utils::connection_context_base& context);
     virtual bool invoke_notify_to_peer(int command, const epee::span<const uint8_t> req_buff, const epee::net_utils::connection_context_base& context);
     virtual bool drop_connection(const epee::net_utils::connection_context_base& context);
@@ -540,6 +549,7 @@ namespace nodetool
     extern const command_line::arg_descriptor<int64_t> arg_limit_rate_up;
     extern const command_line::arg_descriptor<int64_t> arg_limit_rate_down;
     extern const command_line::arg_descriptor<int64_t> arg_limit_rate;
+    extern const command_line::arg_descriptor<bool> arg_pad_transactions;
 }
 
 POP_WARNINGS
diff --git a/src/p2p/net_node.inl b/src/p2p/net_node.inl
index 45bb10593..5dab251b5 100644
--- a/src/p2p/net_node.inl
+++ b/src/p2p/net_node.inl
@@ -116,6 +116,7 @@ namespace nodetool
     command_line::add_arg(desc, arg_limit_rate_up);
     command_line::add_arg(desc, arg_limit_rate_down);
     command_line::add_arg(desc, arg_limit_rate);
+    command_line::add_arg(desc, arg_pad_transactions);
   }
   //-----------------------------------------------------------------------------------
   template<class t_payload_net_handler>
@@ -340,6 +341,7 @@ namespace nodetool
   {
     bool testnet = command_line::get_arg(vm, cryptonote::arg_testnet_on);
     bool stagenet = command_line::get_arg(vm, cryptonote::arg_stagenet_on);
+    const bool pad_txs = command_line::get_arg(vm, arg_pad_transactions);
     m_nettype = testnet ? cryptonote::TESTNET : stagenet ? cryptonote::STAGENET : cryptonote::MAINNET;
 
     network_zone& public_zone = m_network_zones[epee::net_utils::zone::public_];
@@ -384,7 +386,7 @@ namespace nodetool
     m_use_ipv6 = command_line::get_arg(vm, arg_p2p_use_ipv6);
     m_require_ipv4 = !command_line::get_arg(vm, arg_p2p_ignore_ipv4);
     public_zone.m_notifier = cryptonote::levin::notify{
-      public_zone.m_net_server.get_io_service(), public_zone.m_net_server.get_config_shared(), nullptr, true
+      public_zone.m_net_server.get_io_service(), public_zone.m_net_server.get_config_shared(), nullptr, true, pad_txs
     };
 
     if (command_line::has_arg(vm, arg_p2p_add_peer))
@@ -495,7 +497,7 @@ namespace nodetool
       }
 
       zone.m_notifier = cryptonote::levin::notify{
-        zone.m_net_server.get_io_service(), zone.m_net_server.get_config_shared(), std::move(this_noise), false
+        zone.m_net_server.get_io_service(), zone.m_net_server.get_config_shared(), std::move(this_noise), false, pad_txs
       };
     }
 
@@ -2053,18 +2055,18 @@ namespace nodetool
   }
   //-----------------------------------------------------------------------------------
   template<class t_payload_net_handler>
-  epee::net_utils::zone node_server<t_payload_net_handler>::send_txs(std::vector<cryptonote::blobdata> txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, cryptonote::i_core_events& core, const bool pad_txs)
+  epee::net_utils::zone node_server<t_payload_net_handler>::send_txs(std::vector<cryptonote::blobdata> txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, cryptonote::i_core_events& core)
   {
     namespace enet = epee::net_utils;
 
-    const auto send = [&txs, &source, &core, pad_txs] (std::pair<const enet::zone, network_zone>& network)
+    const auto send = [&txs, &source, &core] (std::pair<const enet::zone, network_zone>& network)
     {
       const bool is_public = (network.first == enet::zone::public_);
       const cryptonote::relay_method tx_relay = is_public ?
-        cryptonote::relay_method::flood : cryptonote::relay_method::local;
+        cryptonote::relay_method::fluff : cryptonote::relay_method::local;
 
       core.on_transactions_relayed(epee::to_span(txs), tx_relay);
-      if (network.second.m_notifier.send_txs(std::move(txs), source, (pad_txs || !is_public)))
+      if (network.second.m_notifier.send_txs(std::move(txs), source))
         return network.first;
       return enet::zone::invalid;
     };
diff --git a/src/p2p/net_node_common.h b/src/p2p/net_node_common.h
index aa1b83b83..ed88aa28c 100644
--- a/src/p2p/net_node_common.h
+++ b/src/p2p/net_node_common.h
@@ -50,7 +50,7 @@ namespace nodetool
   struct i_p2p_endpoint
   {
     virtual bool relay_notify_to_list(int command, const epee::span<const uint8_t> data_buff, std::vector<std::pair<epee::net_utils::zone, boost::uuids::uuid>> connections)=0;
-    virtual epee::net_utils::zone send_txs(std::vector<cryptonote::blobdata> txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, cryptonote::i_core_events& core, bool pad_txs)=0;
+    virtual epee::net_utils::zone send_txs(std::vector<cryptonote::blobdata> txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, cryptonote::i_core_events& core)=0;
     virtual bool invoke_command_to_peer(int command, const epee::span<const uint8_t> req_buff, std::string& resp_buff, const epee::net_utils::connection_context_base& context)=0;
     virtual bool invoke_notify_to_peer(int command, const epee::span<const uint8_t> req_buff, const epee::net_utils::connection_context_base& context)=0;
     virtual bool drop_connection(const epee::net_utils::connection_context_base& context)=0;
@@ -75,7 +75,7 @@ namespace nodetool
     {
       return false;
     }
-    virtual epee::net_utils::zone send_txs(std::vector<cryptonote::blobdata> txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, cryptonote::i_core_events& core, const bool pad_txs)
+    virtual epee::net_utils::zone send_txs(std::vector<cryptonote::blobdata> txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, cryptonote::i_core_events& core)
     {
       return epee::net_utils::zone::invalid;
     }
diff --git a/tests/core_tests/chaingen.h b/tests/core_tests/chaingen.h
index bc0e61365..1594d7a08 100644
--- a/tests/core_tests/chaingen.h
+++ b/tests/core_tests/chaingen.h
@@ -511,7 +511,7 @@ public:
     , m_events(events)
     , m_validator(validator)
     , m_ev_index(0)
-    , m_tx_relay(cryptonote::relay_method::flood)
+    , m_tx_relay(cryptonote::relay_method::fluff)
   {
   }
 
@@ -544,7 +544,7 @@ public:
     }
     else
     {
-      m_tx_relay = cryptonote::relay_method::flood;
+      m_tx_relay = cryptonote::relay_method::fluff;
     }
 
     return true;
diff --git a/tests/unit_tests/levin.cpp b/tests/unit_tests/levin.cpp
index e5ca4e41e..38707f075 100644
--- a/tests/unit_tests/levin.cpp
+++ b/tests/unit_tests/levin.cpp
@@ -271,12 +271,12 @@ namespace
             EXPECT_EQ(connection_ids_.size(), connections_->get_connections_count());
         }
 
-        cryptonote::levin::notify make_notifier(const std::size_t noise_size, bool is_public)
+        cryptonote::levin::notify make_notifier(const std::size_t noise_size, bool is_public, bool pad_txs)
         {
             epee::byte_slice noise = nullptr;
             if (noise_size)
                 noise = epee::levin::make_noise_notify(noise_size);
-            return cryptonote::levin::notify{io_service_, connections_, std::move(noise), is_public};
+            return cryptonote::levin::notify{io_service_, connections_, std::move(noise), is_public, pad_txs};
         }
 
         boost::uuids::random_generator random_generator_;
@@ -434,12 +434,16 @@ TEST_F(levin_notify, defaulted)
         EXPECT_FALSE(status.has_noise);
         EXPECT_FALSE(status.connections_filled);
     }
-    EXPECT_FALSE(notifier.send_txs({}, random_generator_(), false));
+    EXPECT_TRUE(notifier.send_txs({}, random_generator_()));
+
+    std::vector<cryptonote::blobdata> txs(2);
+    txs[0].resize(100, 'e');
+    EXPECT_FALSE(notifier.send_txs(std::move(txs), random_generator_()));
 }
 
-TEST_F(levin_notify, flood)
+TEST_F(levin_notify, fluff_without_padding)
 {
-    cryptonote::levin::notify notifier = make_notifier(0, true);
+    cryptonote::levin::notify notifier = make_notifier(0, true, false);
 
     for (unsigned count = 0; count < 10; ++count)
         add_connection(count % 2 == 0);
@@ -464,10 +468,13 @@ TEST_F(levin_notify, flood)
     ASSERT_EQ(10u, contexts_.size());
     {
         auto context = contexts_.begin();
-        EXPECT_TRUE(notifier.send_txs(txs, context->get_id(), false));
+        EXPECT_TRUE(notifier.send_txs(txs, context->get_id()));
 
         io_service_.reset();
         ASSERT_LT(0u, io_service_.poll());
+        notifier.run_fluff();
+        ASSERT_LT(0u, io_service_.poll());
+
         EXPECT_EQ(0u, context->process_send_queue());
         for (++context; context != contexts_.end(); ++context)
             EXPECT_EQ(1u, context->process_send_queue());
@@ -480,14 +487,42 @@ TEST_F(levin_notify, flood)
             EXPECT_TRUE(notification._.empty());
         }
     }
+}
+
+TEST_F(levin_notify, fluff_with_padding)
+{
+    cryptonote::levin::notify notifier = make_notifier(0, true, true);
+
+    for (unsigned count = 0; count < 10; ++count)
+        add_connection(count % 2 == 0);
+
+    {
+        const auto status = notifier.get_status();
+        EXPECT_FALSE(status.has_noise);
+        EXPECT_FALSE(status.connections_filled);
+    }
+    notifier.new_out_connection();
+    io_service_.poll();
+    {
+        const auto status = notifier.get_status();
+        EXPECT_FALSE(status.has_noise);
+        EXPECT_FALSE(status.connections_filled); // not tracked
+    }
+
+    std::vector<cryptonote::blobdata> txs(2);
+    txs[0].resize(100, 'e');
+    txs[1].resize(200, 'f');
 
     ASSERT_EQ(10u, contexts_.size());
     {
         auto context = contexts_.begin();
-        EXPECT_TRUE(notifier.send_txs(txs, context->get_id(), true));
+        EXPECT_TRUE(notifier.send_txs(txs, context->get_id()));
 
         io_service_.reset();
         ASSERT_LT(0u, io_service_.poll());
+        notifier.run_fluff();
+        ASSERT_LT(0u, io_service_.poll());
+
         EXPECT_EQ(0u, context->process_send_queue());
         for (++context; context != contexts_.end(); ++context)
             EXPECT_EQ(1u, context->process_send_queue());
@@ -502,9 +537,9 @@ TEST_F(levin_notify, flood)
     }
 }
 
-TEST_F(levin_notify, private_flood)
+TEST_F(levin_notify, private_fluff_without_padding)
 {
-    cryptonote::levin::notify notifier = make_notifier(0, false);
+    cryptonote::levin::notify notifier = make_notifier(0, false, false);
 
     for (unsigned count = 0; count < 10; ++count)
         add_connection(count % 2 == 0);
@@ -529,10 +564,14 @@ TEST_F(levin_notify, private_flood)
     ASSERT_EQ(10u, contexts_.size());
     {
         auto context = contexts_.begin();
-        EXPECT_TRUE(notifier.send_txs(txs, context->get_id(), false));
+        EXPECT_TRUE(notifier.send_txs(txs, context->get_id()));
 
         io_service_.reset();
         ASSERT_LT(0u, io_service_.poll());
+        notifier.run_fluff();
+        io_service_.reset();
+        ASSERT_LT(0u, io_service_.poll());
+
         EXPECT_EQ(0u, context->process_send_queue());
         for (++context; context != contexts_.end(); ++context)
         {
@@ -548,14 +587,43 @@ TEST_F(levin_notify, private_flood)
             EXPECT_TRUE(notification._.empty());
         }
     }
+}
+
+TEST_F(levin_notify, private_fluff_with_padding)
+{
+    cryptonote::levin::notify notifier = make_notifier(0, false, true);
+
+    for (unsigned count = 0; count < 10; ++count)
+        add_connection(count % 2 == 0);
+
+    {
+        const auto status = notifier.get_status();
+        EXPECT_FALSE(status.has_noise);
+        EXPECT_FALSE(status.connections_filled);
+    }
+    notifier.new_out_connection();
+    io_service_.poll();
+    {
+        const auto status = notifier.get_status();
+        EXPECT_FALSE(status.has_noise);
+        EXPECT_FALSE(status.connections_filled); // not tracked
+    }
+
+    std::vector<cryptonote::blobdata> txs(2);
+    txs[0].resize(100, 'e');
+    txs[1].resize(200, 'f');
 
     ASSERT_EQ(10u, contexts_.size());
     {
         auto context = contexts_.begin();
-        EXPECT_TRUE(notifier.send_txs(txs, context->get_id(), true));
+        EXPECT_TRUE(notifier.send_txs(txs, context->get_id()));
 
         io_service_.reset();
         ASSERT_LT(0u, io_service_.poll());
+        notifier.run_fluff();
+        io_service_.reset();
+        ASSERT_LT(0u, io_service_.poll());
+
         EXPECT_EQ(0u, context->process_send_queue());
         for (++context; context != contexts_.end(); ++context)
         {
@@ -582,7 +650,7 @@ TEST_F(levin_notify, noise)
     txs[0].resize(1900, 'h');
 
     const boost::uuids::uuid incoming_id = random_generator_();
-    cryptonote::levin::notify notifier = make_notifier(2048, false);
+    cryptonote::levin::notify notifier = make_notifier(2048, false, true);
 
     {
         const auto status = notifier.get_status();
@@ -608,7 +676,7 @@ TEST_F(levin_notify, noise)
         EXPECT_EQ(0u, receiver_.notified_size());
     }
 
-    EXPECT_TRUE(notifier.send_txs(txs, incoming_id, false));
+    EXPECT_TRUE(notifier.send_txs(txs, incoming_id));
     notifier.run_stems();
     io_service_.reset();
     ASSERT_LT(0u, io_service_.poll());
@@ -627,7 +695,7 @@ TEST_F(levin_notify, noise)
     }
 
     txs[0].resize(3000, 'r');
-    EXPECT_TRUE(notifier.send_txs(txs, incoming_id, true));
+    EXPECT_TRUE(notifier.send_txs(txs, incoming_id));
     notifier.run_stems();
     io_service_.reset();
     ASSERT_LT(0u, io_service_.poll());