diff --git a/bridges/bin/millau/runtime/src/lib.rs b/bridges/bin/millau/runtime/src/lib.rs
index fecbf4f99073fa4671496e570a5eb3ab6c0bea2e..8b2988d421fdc74c19c1ac73eb5131509b521bea 100644
--- a/bridges/bin/millau/runtime/src/lib.rs
+++ b/bridges/bin/millau/runtime/src/lib.rs
@@ -453,6 +453,7 @@ impl pallet_bridge_messages::Config<WithRialtoMessagesInstance> for Runtime {
 	type MaxUnrewardedRelayerEntriesAtInboundLane = MaxUnrewardedRelayerEntriesAtInboundLane;
 	type MaxUnconfirmedMessagesAtInboundLane = MaxUnconfirmedMessagesAtInboundLane;
 
+	type MaximalOutboundPayloadSize = crate::rialto_messages::ToRialtoMaximalOutboundPayloadSize;
 	type OutboundPayload = crate::rialto_messages::ToRialtoMessagePayload;
 	type OutboundMessageFee = Balance;
 
@@ -484,6 +485,8 @@ impl pallet_bridge_messages::Config<WithRialtoParachainMessagesInstance> for Run
 	type MaxUnrewardedRelayerEntriesAtInboundLane = MaxUnrewardedRelayerEntriesAtInboundLane;
 	type MaxUnconfirmedMessagesAtInboundLane = MaxUnconfirmedMessagesAtInboundLane;
 
+	type MaximalOutboundPayloadSize =
+		crate::rialto_parachain_messages::ToRialtoParachainMaximalOutboundPayloadSize;
 	type OutboundPayload = crate::rialto_parachain_messages::ToRialtoParachainMessagePayload;
 	type OutboundMessageFee = Balance;
 
diff --git a/bridges/bin/millau/runtime/src/rialto_messages.rs b/bridges/bin/millau/runtime/src/rialto_messages.rs
index dad7591147d81d708a7609d2080897287df89ee1..bbcc6144f8521bc65333c85d15fb8842b9066c6c 100644
--- a/bridges/bin/millau/runtime/src/rialto_messages.rs
+++ b/bridges/bin/millau/runtime/src/rialto_messages.rs
@@ -79,6 +79,10 @@ pub type FromRialtoMessageDispatch = messages::target::FromBridgedChainMessageDi
 	frame_support::traits::ConstU64<BASE_XCM_WEIGHT_TWICE>,
 >;
 
+/// Maximal outbound payload size of Millau -> Rialto messages.
+pub type ToRialtoMaximalOutboundPayloadSize =
+	messages::source::FromThisChainMaximalOutboundPayloadSize<WithRialtoMessageBridge>;
+
 /// Millau <-> Rialto message bridge.
 #[derive(RuntimeDebug, Clone, Copy)]
 pub struct WithRialtoMessageBridge;
@@ -145,12 +149,9 @@ impl messages::ThisChainWithMessages for Millau {
 	}
 
 	fn estimate_delivery_confirmation_transaction() -> MessageTransaction<Weight> {
-		let inbound_data_size = InboundLaneData::<bp_millau::AccountId>::encoded_size_hint(
-			bp_millau::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE,
-			1,
-			1,
-		)
-		.unwrap_or(u32::MAX);
+		let inbound_data_size = InboundLaneData::<bp_millau::AccountId>::encoded_size_hint(1, 1)
+			.and_then(|x| u32::try_from(x).ok())
+			.unwrap_or(u32::MAX);
 
 		MessageTransaction {
 			dispatch_weight: bp_millau::MAX_SINGLE_MESSAGE_DELIVERY_CONFIRMATION_TX_WEIGHT,
@@ -346,10 +347,10 @@ mod tests {
 
 		let max_incoming_inbound_lane_data_proof_size =
 			bp_messages::InboundLaneData::<()>::encoded_size_hint(
-				bp_millau::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE,
 				bp_millau::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX as _,
 				bp_millau::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX as _,
 			)
+			.and_then(|x| u32::try_from(x).ok())
 			.unwrap_or(u32::MAX);
 		pallet_bridge_messages::ensure_able_to_receive_confirmation::<Weights>(
 			bp_millau::Millau::max_extrinsic_size(),
diff --git a/bridges/bin/millau/runtime/src/rialto_parachain_messages.rs b/bridges/bin/millau/runtime/src/rialto_parachain_messages.rs
index 26b2b5d4c48b7556cf9645fcbf8ef3f68fa91e3a..ceabe2b00018d100803606c6c598b9505377fbd7 100644
--- a/bridges/bin/millau/runtime/src/rialto_parachain_messages.rs
+++ b/bridges/bin/millau/runtime/src/rialto_parachain_messages.rs
@@ -84,6 +84,10 @@ pub type FromRialtoParachainMessageDispatch = messages::target::FromBridgedChain
 	frame_support::traits::ConstU64<BASE_XCM_WEIGHT_TWICE>,
 >;
 
+/// Maximal outbound payload size of Millau -> RialtoParachain messages.
+pub type ToRialtoParachainMaximalOutboundPayloadSize =
+	messages::source::FromThisChainMaximalOutboundPayloadSize<WithRialtoParachainMessageBridge>;
+
 /// Millau <-> RialtoParachain message bridge.
 #[derive(RuntimeDebug, Clone, Copy)]
 pub struct WithRialtoParachainMessageBridge;
@@ -134,12 +138,9 @@ impl messages::ThisChainWithMessages for Millau {
 	}
 
 	fn estimate_delivery_confirmation_transaction() -> MessageTransaction<Weight> {
-		let inbound_data_size = InboundLaneData::<bp_millau::AccountId>::encoded_size_hint(
-			bp_millau::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE,
-			1,
-			1,
-		)
-		.unwrap_or(u32::MAX);
+		let inbound_data_size = InboundLaneData::<bp_millau::AccountId>::encoded_size_hint(1, 1)
+			.and_then(|x| u32::try_from(x).ok())
+			.unwrap_or(u32::MAX);
 
 		MessageTransaction {
 			dispatch_weight: bp_millau::MAX_SINGLE_MESSAGE_DELIVERY_CONFIRMATION_TX_WEIGHT,
diff --git a/bridges/bin/rialto-parachain/runtime/src/lib.rs b/bridges/bin/rialto-parachain/runtime/src/lib.rs
index 0927f138f3baf3b329e8b7e318053e0cc88ed80f..37930c3555412d816cb841799a2db93424403782 100644
--- a/bridges/bin/rialto-parachain/runtime/src/lib.rs
+++ b/bridges/bin/rialto-parachain/runtime/src/lib.rs
@@ -520,6 +520,7 @@ impl pallet_bridge_messages::Config<WithMillauMessagesInstance> for Runtime {
 	type MaxUnrewardedRelayerEntriesAtInboundLane = MaxUnrewardedRelayerEntriesAtInboundLane;
 	type MaxUnconfirmedMessagesAtInboundLane = MaxUnconfirmedMessagesAtInboundLane;
 
+	type MaximalOutboundPayloadSize = crate::millau_messages::ToMillauMaximalOutboundPayloadSize;
 	type OutboundPayload = crate::millau_messages::ToMillauMessagePayload;
 	type OutboundMessageFee = Balance;
 
diff --git a/bridges/bin/rialto-parachain/runtime/src/millau_messages.rs b/bridges/bin/rialto-parachain/runtime/src/millau_messages.rs
index 8d88a4d44bcc93ae9457d380753ed8bfa901bc5a..ef8e3c657ac693ce565119675b0977f8070931ea 100644
--- a/bridges/bin/rialto-parachain/runtime/src/millau_messages.rs
+++ b/bridges/bin/rialto-parachain/runtime/src/millau_messages.rs
@@ -82,6 +82,10 @@ pub type FromMillauMessagesProof = messages::target::FromBridgedChainMessagesPro
 pub type ToMillauMessagesDeliveryProof =
 	messages::source::FromBridgedChainMessagesDeliveryProof<bp_millau::Hash>;
 
+/// Maximal outbound payload size of Rialto -> Millau messages.
+pub type ToMillauMaximalOutboundPayloadSize =
+	messages::source::FromThisChainMaximalOutboundPayloadSize<WithMillauMessageBridge>;
+
 /// Millau <-> RialtoParachain message bridge.
 #[derive(RuntimeDebug, Clone, Copy)]
 pub struct WithMillauMessageBridge;
@@ -134,12 +138,9 @@ impl messages::ThisChainWithMessages for RialtoParachain {
 
 	fn estimate_delivery_confirmation_transaction() -> MessageTransaction<Weight> {
 		let inbound_data_size =
-			InboundLaneData::<bp_rialto_parachain::AccountId>::encoded_size_hint(
-				bp_rialto_parachain::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE,
-				1,
-				1,
-			)
-			.unwrap_or(u32::MAX);
+			InboundLaneData::<bp_rialto_parachain::AccountId>::encoded_size_hint(1, 1)
+				.and_then(|x| u32::try_from(x).ok())
+				.unwrap_or(u32::MAX);
 
 		MessageTransaction {
 			dispatch_weight:
diff --git a/bridges/bin/rialto/runtime/src/lib.rs b/bridges/bin/rialto/runtime/src/lib.rs
index e10ddda0fb3a91b9acc7a39740af74a7e65084f6..3d468a5b4ec1cca4f4348d150d33a5b2a59602b3 100644
--- a/bridges/bin/rialto/runtime/src/lib.rs
+++ b/bridges/bin/rialto/runtime/src/lib.rs
@@ -437,6 +437,7 @@ impl pallet_bridge_messages::Config<WithMillauMessagesInstance> for Runtime {
 	type MaxUnrewardedRelayerEntriesAtInboundLane = MaxUnrewardedRelayerEntriesAtInboundLane;
 	type MaxUnconfirmedMessagesAtInboundLane = MaxUnconfirmedMessagesAtInboundLane;
 
+	type MaximalOutboundPayloadSize = crate::millau_messages::ToMillauMaximalOutboundPayloadSize;
 	type OutboundPayload = crate::millau_messages::ToMillauMessagePayload;
 	type OutboundMessageFee = Balance;
 
diff --git a/bridges/bin/rialto/runtime/src/millau_messages.rs b/bridges/bin/rialto/runtime/src/millau_messages.rs
index fdc376be70e6ea4efad8d250f3dffba7b7549aa2..d547dde538fec8a8868f7a514bb00ee2e40e35ac 100644
--- a/bridges/bin/rialto/runtime/src/millau_messages.rs
+++ b/bridges/bin/rialto/runtime/src/millau_messages.rs
@@ -78,6 +78,10 @@ pub type FromMillauMessagesProof = messages::target::FromBridgedChainMessagesPro
 pub type ToMillauMessagesDeliveryProof =
 	messages::source::FromBridgedChainMessagesDeliveryProof<bp_millau::Hash>;
 
+/// Maximal outbound payload size of Rialto -> Millau messages.
+pub type ToMillauMaximalOutboundPayloadSize =
+	messages::source::FromThisChainMaximalOutboundPayloadSize<WithMillauMessageBridge>;
+
 /// Millau <-> Rialto message bridge.
 #[derive(RuntimeDebug, Clone, Copy)]
 pub struct WithMillauMessageBridge;
@@ -144,12 +148,9 @@ impl messages::ThisChainWithMessages for Rialto {
 	}
 
 	fn estimate_delivery_confirmation_transaction() -> MessageTransaction<Weight> {
-		let inbound_data_size = InboundLaneData::<bp_rialto::AccountId>::encoded_size_hint(
-			bp_rialto::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE,
-			1,
-			1,
-		)
-		.unwrap_or(u32::MAX);
+		let inbound_data_size = InboundLaneData::<bp_rialto::AccountId>::encoded_size_hint(1, 1)
+			.and_then(|x| u32::try_from(x).ok())
+			.unwrap_or(u32::MAX);
 
 		MessageTransaction {
 			dispatch_weight: bp_rialto::MAX_SINGLE_MESSAGE_DELIVERY_CONFIRMATION_TX_WEIGHT,
@@ -343,10 +344,10 @@ mod tests {
 
 		let max_incoming_inbound_lane_data_proof_size =
 			bp_messages::InboundLaneData::<()>::encoded_size_hint(
-				bp_rialto::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE,
 				bp_rialto::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX as _,
 				bp_rialto::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX as _,
 			)
+			.and_then(|x| u32::try_from(x).ok())
 			.unwrap_or(u32::MAX);
 		pallet_bridge_messages::ensure_able_to_receive_confirmation::<Weights>(
 			bp_rialto::Rialto::max_extrinsic_size(),
diff --git a/bridges/bin/runtime-common/src/messages.rs b/bridges/bin/runtime-common/src/messages.rs
index a9a252118b254ebc38d99b63350dd033a70e9edb..e8f8fc1f7f03805ce24b8be1a9f19e937bd8b963 100644
--- a/bridges/bin/runtime-common/src/messages.rs
+++ b/bridges/bin/runtime-common/src/messages.rs
@@ -200,6 +200,15 @@ pub mod source {
 	/// Message payload for This -> Bridged chain messages.
 	pub type FromThisChainMessagePayload = Vec<u8>;
 
+	/// Maximal size of outbound message payload.
+	pub struct FromThisChainMaximalOutboundPayloadSize<B>(PhantomData<B>);
+
+	impl<B: MessageBridge> Get<u32> for FromThisChainMaximalOutboundPayloadSize<B> {
+		fn get() -> u32 {
+			maximal_message_size::<B>()
+		}
+	}
+
 	/// Messages delivery proof from bridged chain:
 	///
 	/// - hash of finalized header;
@@ -216,7 +225,7 @@ pub mod source {
 	}
 
 	impl<BridgedHeaderHash> Size for FromBridgedChainMessagesDeliveryProof<BridgedHeaderHash> {
-		fn size_hint(&self) -> u32 {
+		fn size(&self) -> u32 {
 			u32::try_from(
 				self.storage_proof
 					.iter()
@@ -529,7 +538,7 @@ pub mod target {
 	}
 
 	impl<BridgedHeaderHash> Size for FromBridgedChainMessagesProof<BridgedHeaderHash> {
-		fn size_hint(&self) -> u32 {
+		fn size(&self) -> u32 {
 			u32::try_from(
 				self.storage_proof
 					.iter()
diff --git a/bridges/modules/messages/src/inbound_lane.rs b/bridges/modules/messages/src/inbound_lane.rs
index 00875bb878a823beda55a136ab96910ea8eceaaf..6624655ddc18e9ae3b73fbf576e197f5ae1ee6c5 100644
--- a/bridges/modules/messages/src/inbound_lane.rs
+++ b/bridges/modules/messages/src/inbound_lane.rs
@@ -16,13 +16,17 @@
 
 //! Everything about incoming messages receival.
 
+use crate::Config;
+
 use bp_messages::{
 	target_chain::{DispatchMessage, DispatchMessageData, MessageDispatch},
 	DeliveredMessages, InboundLaneData, LaneId, MessageKey, MessageNonce, OutboundLaneData,
 	UnrewardedRelayer,
 };
 use bp_runtime::messages::MessageDispatchResult;
-use frame_support::RuntimeDebug;
+use codec::{Decode, Encode, EncodeLike, MaxEncodedLen};
+use frame_support::{traits::Get, RuntimeDebug};
+use scale_info::{Type, TypeInfo};
 use sp_std::prelude::PartialEq;
 
 /// Inbound lane storage.
@@ -44,6 +48,76 @@ pub trait InboundLaneStorage {
 	fn set_data(&mut self, data: InboundLaneData<Self::Relayer>);
 }
 
+/// Inbound lane data wrapper that implements `MaxEncodedLen`.
+///
+/// We have already had `MaxEncodedLen`-like functionality before, but its usage has
+/// been localized and we haven't been passing bounds (maximal count of unrewarded relayer entries,
+/// maximal count of unconfirmed messages) everywhere. This wrapper allows us to avoid passing
+/// these generic bounds all over the code.
+///
+/// The encoding of this type matches encoding of the corresponding `MessageData`.
+#[derive(Encode, Decode, Clone, RuntimeDebug, PartialEq, Eq)]
+pub struct StoredInboundLaneData<T: Config<I>, I: 'static>(pub InboundLaneData<T::InboundRelayer>);
+
+impl<T: Config<I>, I: 'static> sp_std::ops::Deref for StoredInboundLaneData<T, I> {
+	type Target = InboundLaneData<T::InboundRelayer>;
+
+	fn deref(&self) -> &Self::Target {
+		&self.0
+	}
+}
+
+impl<T: Config<I>, I: 'static> sp_std::ops::DerefMut for StoredInboundLaneData<T, I> {
+	fn deref_mut(&mut self) -> &mut Self::Target {
+		&mut self.0
+	}
+}
+
+impl<T: Config<I>, I: 'static> Default for StoredInboundLaneData<T, I> {
+	fn default() -> Self {
+		StoredInboundLaneData(Default::default())
+	}
+}
+
+impl<T: Config<I>, I: 'static> From<InboundLaneData<T::InboundRelayer>>
+	for StoredInboundLaneData<T, I>
+{
+	fn from(data: InboundLaneData<T::InboundRelayer>) -> Self {
+		StoredInboundLaneData(data)
+	}
+}
+
+impl<T: Config<I>, I: 'static> From<StoredInboundLaneData<T, I>>
+	for InboundLaneData<T::InboundRelayer>
+{
+	fn from(data: StoredInboundLaneData<T, I>) -> Self {
+		data.0
+	}
+}
+
+impl<T: Config<I>, I: 'static> EncodeLike<StoredInboundLaneData<T, I>>
+	for InboundLaneData<T::InboundRelayer>
+{
+}
+
+impl<T: Config<I>, I: 'static> TypeInfo for StoredInboundLaneData<T, I> {
+	type Identity = Self;
+
+	fn type_info() -> Type {
+		InboundLaneData::<T::InboundRelayer>::type_info()
+	}
+}
+
+impl<T: Config<I>, I: 'static> MaxEncodedLen for StoredInboundLaneData<T, I> {
+	fn max_encoded_len() -> usize {
+		InboundLaneData::<T::InboundRelayer>::encoded_size_hint(
+			T::MaxUnrewardedRelayerEntriesAtInboundLane::get() as usize,
+			T::MaxUnconfirmedMessagesAtInboundLane::get() as usize,
+		)
+		.unwrap_or(usize::MAX)
+	}
+}
+
 /// Result of single message receival.
 #[derive(RuntimeDebug, PartialEq, Eq)]
 pub enum ReceivalResult {
@@ -333,7 +407,7 @@ mod tests {
 		run_test(|| {
 			let mut lane = inbound_lane::<TestRuntime, _>(TEST_LANE_ID);
 			let max_nonce =
-				<TestRuntime as crate::Config>::MaxUnrewardedRelayerEntriesAtInboundLane::get();
+				<TestRuntime as Config>::MaxUnrewardedRelayerEntriesAtInboundLane::get();
 			for current_nonce in 1..max_nonce + 1 {
 				assert_eq!(
 					lane.receive_message::<TestMessageDispatch, _>(
@@ -372,8 +446,7 @@ mod tests {
 	fn fails_to_receive_messages_above_unconfirmed_messages_limit_per_lane() {
 		run_test(|| {
 			let mut lane = inbound_lane::<TestRuntime, _>(TEST_LANE_ID);
-			let max_nonce =
-				<TestRuntime as crate::Config>::MaxUnconfirmedMessagesAtInboundLane::get();
+			let max_nonce = <TestRuntime as Config>::MaxUnconfirmedMessagesAtInboundLane::get();
 			for current_nonce in 1..=max_nonce {
 				assert_eq!(
 					lane.receive_message::<TestMessageDispatch, _>(
diff --git a/bridges/modules/messages/src/lib.rs b/bridges/modules/messages/src/lib.rs
index a9901018187185d4a81a8910411f796827ebfc00..e02c94d022768bd4ea4740c5769d60da2b84c305 100644
--- a/bridges/modules/messages/src/lib.rs
+++ b/bridges/modules/messages/src/lib.rs
@@ -37,6 +37,8 @@
 // Generated by `decl_event!`
 #![allow(clippy::unused_unit)]
 
+pub use inbound_lane::StoredInboundLaneData;
+pub use outbound_lane::StoredMessageData;
 pub use weights::WeightInfo;
 pub use weights_ext::{
 	ensure_able_to_receive_confirmation, ensure_able_to_receive_message,
@@ -62,9 +64,9 @@ use bp_messages::{
 	UnrewardedRelayersState,
 };
 use bp_runtime::{BasicOperatingMode, ChainId, OwnedBridgeModule, Size};
-use codec::{Decode, Encode};
+use codec::{Decode, Encode, MaxEncodedLen};
 use frame_support::{
-	fail,
+	ensure, fail,
 	traits::Get,
 	weights::{Pays, PostDispatchInfo},
 };
@@ -145,6 +147,9 @@ pub mod pallet {
 		/// these messages are from different lanes.
 		type MaxUnconfirmedMessagesAtInboundLane: Get<MessageNonce>;
 
+		/// Maximal size of the outbound payload.
+		#[pallet::constant]
+		type MaximalOutboundPayloadSize: Get<u32>;
 		/// Payload type of outbound messages. This payload is dispatched on the bridged chain.
 		type OutboundPayload: Parameter + Size;
 		/// Message fee type of outbound messages. This fee is paid on this chain.
@@ -154,7 +159,8 @@ pub mod pallet {
 			+ Parameter
 			+ SaturatingAdd
 			+ Zero
-			+ Copy;
+			+ Copy
+			+ MaxEncodedLen;
 
 		/// Payload type of inbound messages. This payload is dispatched on this chain.
 		type InboundPayload: Decode;
@@ -162,7 +168,7 @@ pub mod pallet {
 		type InboundMessageFee: Decode + Zero;
 		/// Identifier of relayer that deliver messages to this chain. Relayer reward is paid on the
 		/// bridged chain.
-		type InboundRelayer: Parameter;
+		type InboundRelayer: Parameter + MaxEncodedLen;
 
 		/// A type which can be turned into an AccountId from a 256-bit hash.
 		///
@@ -216,7 +222,6 @@ pub mod pallet {
 
 	#[pallet::pallet]
 	#[pallet::generate_store(pub(super) trait Store)]
-	#[pallet::without_storage_info]
 	pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
 
 	impl<T: Config<I>, I: 'static> OwnedBridgeModule<T> for Pallet<T, I> {
@@ -658,6 +663,8 @@ pub mod pallet {
 	pub enum Error<T, I = ()> {
 		/// Pallet is not in Normal operating mode.
 		NotOperatingNormally,
+		/// The message is too large to be sent over the bridge.
+		MessageIsTooLarge,
 		/// Message has been treated as invalid by chain verifier.
 		MessageRejectedByChainVerifier,
 		/// Message has been treated as invalid by lane verifier.
@@ -707,7 +714,7 @@ pub mod pallet {
 	/// Map of lane id => inbound lane data.
 	#[pallet::storage]
 	pub type InboundLanes<T: Config<I>, I: 'static = ()> =
-		StorageMap<_, Blake2_128Concat, LaneId, InboundLaneData<T::InboundRelayer>, ValueQuery>;
+		StorageMap<_, Blake2_128Concat, LaneId, StoredInboundLaneData<T, I>, ValueQuery>;
 
 	/// Map of lane id => outbound lane data.
 	#[pallet::storage]
@@ -717,7 +724,7 @@ pub mod pallet {
 	/// All queued outbound messages.
 	#[pallet::storage]
 	pub type OutboundMessages<T: Config<I>, I: 'static = ()> =
-		StorageMap<_, Blake2_128Concat, MessageKey, MessageData<T::OutboundMessageFee>>;
+		StorageMap<_, Blake2_128Concat, MessageKey, StoredMessageData<T, I>>;
 
 	#[pallet::genesis_config]
 	pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
@@ -756,7 +763,7 @@ pub mod pallet {
 			lane: LaneId,
 			nonce: MessageNonce,
 		) -> Option<MessageData<T::OutboundMessageFee>> {
-			OutboundMessages::<T, I>::get(MessageKey { lane_id: lane, nonce })
+			OutboundMessages::<T, I>::get(MessageKey { lane_id: lane, nonce }).map(Into::into)
 		}
 
 		/// Prepare data, related to given inbound message.
@@ -822,6 +829,12 @@ fn send_message<T: Config<I>, I: 'static>(
 > {
 	ensure_normal_operating_mode::<T, I>()?;
 
+	// the most lightweigh check is the message size check
+	ensure!(
+		payload.size() < T::MaximalOutboundPayloadSize::get(),
+		Error::<T, I>::MessageIsTooLarge,
+	);
+
 	// initially, actual (post-dispatch) weight is equal to pre-dispatch weight
 	let mut actual_weight = T::WeightInfo::send_message_weight(&payload, T::DbWeight::get());
 
@@ -955,7 +968,8 @@ where
 		// this loop is bound by `T::MaxUnconfirmedMessagesAtInboundLane` on the bridged chain
 		let mut relayer_reward = relayers_rewards.entry(entry.relayer).or_default();
 		for nonce in nonce_begin..nonce_end + 1 {
-			let message_data = OutboundMessages::<T, I>::get(MessageKey { lane_id, nonce })
+			let key = MessageKey { lane_id, nonce };
+			let message_data = OutboundMessages::<T, I>::get(key)
 				.expect("message was just confirmed; we never prune unconfirmed messages; qed");
 			relayer_reward.reward = relayer_reward.reward.saturating_add(&message_data.fee);
 			relayer_reward.messages += 1;
@@ -1027,7 +1041,8 @@ impl<T: Config<I>, I: 'static> InboundLaneStorage for RuntimeInboundLaneStorage<
 		match self.cached_data.clone().into_inner() {
 			Some(data) => data,
 			None => {
-				let data = InboundLanes::<T, I>::get(&self.lane_id);
+				let data: InboundLaneData<T::InboundRelayer> =
+					InboundLanes::<T, I>::get(&self.lane_id).into();
 				*self.cached_data.try_borrow_mut().expect(
 					"we're in the single-threaded environment;\
 						we have no recursive borrows; qed",
@@ -1042,7 +1057,7 @@ impl<T: Config<I>, I: 'static> InboundLaneStorage for RuntimeInboundLaneStorage<
 			"we're in the single-threaded environment;\
 				we have no recursive borrows; qed",
 		) = Some(data.clone());
-		InboundLanes::<T, I>::insert(&self.lane_id, data)
+		InboundLanes::<T, I>::insert(&self.lane_id, StoredInboundLaneData::<T, I>(data))
 	}
 }
 
@@ -1070,6 +1085,7 @@ impl<T: Config<I>, I: 'static> OutboundLaneStorage for RuntimeOutboundLaneStorag
 	#[cfg(test)]
 	fn message(&self, nonce: &MessageNonce) -> Option<MessageData<T::OutboundMessageFee>> {
 		OutboundMessages::<T, I>::get(MessageKey { lane_id: self.lane_id, nonce: *nonce })
+			.map(Into::into)
 	}
 
 	fn save_message(
@@ -1116,8 +1132,9 @@ mod tests {
 		message, message_payload, run_test, unrewarded_relayer, Event as TestEvent, Origin,
 		TestMessageDeliveryAndDispatchPayment, TestMessagesDeliveryProof, TestMessagesParameter,
 		TestMessagesProof, TestOnDeliveryConfirmed1, TestOnDeliveryConfirmed2,
-		TestOnMessageAccepted, TestRuntime, TokenConversionRate, PAYLOAD_REJECTED_BY_TARGET_CHAIN,
-		REGULAR_PAYLOAD, TEST_LANE_ID, TEST_RELAYER_A, TEST_RELAYER_B,
+		TestOnMessageAccepted, TestRuntime, TokenConversionRate, MAX_OUTBOUND_PAYLOAD_SIZE,
+		PAYLOAD_REJECTED_BY_TARGET_CHAIN, REGULAR_PAYLOAD, TEST_LANE_ID, TEST_RELAYER_A,
+		TEST_RELAYER_B,
 	};
 	use bp_messages::{UnrewardedRelayer, UnrewardedRelayersState};
 	use bp_test_utils::generate_owned_bridge_module_tests;
@@ -1137,7 +1154,7 @@ mod tests {
 	fn inbound_unrewarded_relayers_state(
 		lane: bp_messages::LaneId,
 	) -> bp_messages::UnrewardedRelayersState {
-		let inbound_lane_data = InboundLanes::<TestRuntime, ()>::get(&lane);
+		let inbound_lane_data = InboundLanes::<TestRuntime, ()>::get(&lane).0;
 		let last_delivered_nonce = inbound_lane_data.last_delivered_nonce();
 		let relayers = inbound_lane_data.relayers;
 		bp_messages::UnrewardedRelayersState {
@@ -1443,6 +1460,27 @@ mod tests {
 		});
 	}
 
+	#[test]
+	fn send_message_rejects_too_large_message() {
+		run_test(|| {
+			let mut message_payload = message_payload(1, 0);
+			// the payload isn't simply extra, so it'll definitely overflow
+			// `MAX_OUTBOUND_PAYLOAD_SIZE` if we add `MAX_OUTBOUND_PAYLOAD_SIZE` bytes to extra
+			message_payload
+				.extra
+				.extend_from_slice(&[0u8; MAX_OUTBOUND_PAYLOAD_SIZE as usize]);
+			assert_noop!(
+				Pallet::<TestRuntime>::send_message(
+					Origin::signed(1),
+					TEST_LANE_ID,
+					message_payload,
+					0,
+				),
+				Error::<TestRuntime, ()>::MessageIsTooLarge,
+			);
+		})
+	}
+
 	#[test]
 	fn chain_verifier_rejects_invalid_message_in_send_message() {
 		run_test(|| {
@@ -1502,7 +1540,7 @@ mod tests {
 				REGULAR_PAYLOAD.declared_weight,
 			));
 
-			assert_eq!(InboundLanes::<TestRuntime>::get(TEST_LANE_ID).last_delivered_nonce(), 1);
+			assert_eq!(InboundLanes::<TestRuntime>::get(TEST_LANE_ID).0.last_delivered_nonce(), 1);
 		});
 	}
 
@@ -1547,7 +1585,7 @@ mod tests {
 			));
 
 			assert_eq!(
-				InboundLanes::<TestRuntime>::get(TEST_LANE_ID),
+				InboundLanes::<TestRuntime>::get(TEST_LANE_ID).0,
 				InboundLaneData {
 					last_confirmed_nonce: 9,
 					relayers: vec![
@@ -2146,8 +2184,8 @@ mod tests {
 		run_test(|| {
 			let mut small_payload = message_payload(0, 100);
 			let mut large_payload = message_payload(1, 100);
-			small_payload.extra = vec![1; 100];
-			large_payload.extra = vec![2; 16_384];
+			small_payload.extra = vec![1; MAX_OUTBOUND_PAYLOAD_SIZE as usize / 10];
+			large_payload.extra = vec![2; MAX_OUTBOUND_PAYLOAD_SIZE as usize / 5];
 
 			assert_ok!(Pallet::<TestRuntime>::send_message(
 				Origin::signed(1),
diff --git a/bridges/modules/messages/src/mock.rs b/bridges/modules/messages/src/mock.rs
index 33c6eae6eb23b06c0f80fbdfe21441e3bea13f9e..b836517e419a24399ce0fcaf034055fdb9a477de 100644
--- a/bridges/modules/messages/src/mock.rs
+++ b/bridges/modules/messages/src/mock.rs
@@ -174,6 +174,7 @@ impl Config for TestRuntime {
 	type MaxUnrewardedRelayerEntriesAtInboundLane = MaxUnrewardedRelayerEntriesAtInboundLane;
 	type MaxUnconfirmedMessagesAtInboundLane = MaxUnconfirmedMessagesAtInboundLane;
 
+	type MaximalOutboundPayloadSize = frame_support::traits::ConstU32<MAX_OUTBOUND_PAYLOAD_SIZE>;
 	type OutboundPayload = TestPayload;
 	type OutboundMessageFee = TestMessageFee;
 
@@ -205,11 +206,14 @@ impl SenderOrigin<AccountId> for Origin {
 }
 
 impl Size for TestPayload {
-	fn size_hint(&self) -> u32 {
+	fn size(&self) -> u32 {
 		16 + self.extra.len() as u32
 	}
 }
 
+/// Maximal outbound payload size.
+pub const MAX_OUTBOUND_PAYLOAD_SIZE: u32 = 4096;
+
 /// Account that has balance to use in tests.
 pub const ENDOWED_ACCOUNT: AccountId = 0xDEAD;
 
@@ -244,7 +248,7 @@ pub struct TestMessagesProof {
 }
 
 impl Size for TestMessagesProof {
-	fn size_hint(&self) -> u32 {
+	fn size(&self) -> u32 {
 		0
 	}
 }
@@ -271,7 +275,7 @@ impl From<Result<Vec<Message<TestMessageFee>>, ()>> for TestMessagesProof {
 pub struct TestMessagesDeliveryProof(pub Result<(LaneId, InboundLaneData<TestRelayer>), ()>);
 
 impl Size for TestMessagesDeliveryProof {
-	fn size_hint(&self) -> u32 {
+	fn size(&self) -> u32 {
 		0
 	}
 }
diff --git a/bridges/modules/messages/src/outbound_lane.rs b/bridges/modules/messages/src/outbound_lane.rs
index 041dec214b11cef1f000c73e60ca0137b7006f73..5f977b2f2e297f0b48936cc145f0f4e12cacae49 100644
--- a/bridges/modules/messages/src/outbound_lane.rs
+++ b/bridges/modules/messages/src/outbound_lane.rs
@@ -16,12 +16,16 @@
 
 //! Everything about outgoing messages sending.
 
+use crate::Config;
+
 use bitvec::prelude::*;
 use bp_messages::{
 	DeliveredMessages, DispatchResultsBitVec, LaneId, MessageData, MessageNonce, OutboundLaneData,
 	UnrewardedRelayer,
 };
-use frame_support::RuntimeDebug;
+use codec::{Decode, Encode, EncodeLike, MaxEncodedLen};
+use frame_support::{traits::Get, RuntimeDebug};
+use scale_info::{Type, TypeInfo};
 use sp_std::collections::vec_deque::VecDeque;
 
 /// Outbound lane storage.
@@ -44,6 +48,66 @@ pub trait OutboundLaneStorage {
 	fn remove_message(&mut self, nonce: &MessageNonce);
 }
 
+/// Outbound message data wrapper that implements `MaxEncodedLen`.
+///
+/// We have already had `MaxEncodedLen`-like functionality before, but its usage has
+/// been localized and we haven't been passing it everywhere. This wrapper allows us
+/// to avoid passing these generic bounds all over the code.
+///
+/// The encoding of this type matches encoding of the corresponding `MessageData`.
+#[derive(Encode, Decode, Clone, RuntimeDebug, PartialEq, Eq)]
+pub struct StoredMessageData<T: Config<I>, I: 'static>(pub MessageData<T::OutboundMessageFee>);
+
+impl<T: Config<I>, I: 'static> sp_std::ops::Deref for StoredMessageData<T, I> {
+	type Target = MessageData<T::OutboundMessageFee>;
+
+	fn deref(&self) -> &Self::Target {
+		&self.0
+	}
+}
+
+impl<T: Config<I>, I: 'static> sp_std::ops::DerefMut for StoredMessageData<T, I> {
+	fn deref_mut(&mut self) -> &mut Self::Target {
+		&mut self.0
+	}
+}
+
+impl<T: Config<I>, I: 'static> From<MessageData<T::OutboundMessageFee>>
+	for StoredMessageData<T, I>
+{
+	fn from(data: MessageData<T::OutboundMessageFee>) -> Self {
+		StoredMessageData(data)
+	}
+}
+
+impl<T: Config<I>, I: 'static> From<StoredMessageData<T, I>>
+	for MessageData<T::OutboundMessageFee>
+{
+	fn from(data: StoredMessageData<T, I>) -> Self {
+		data.0
+	}
+}
+
+impl<T: Config<I>, I: 'static> TypeInfo for StoredMessageData<T, I> {
+	type Identity = Self;
+
+	fn type_info() -> Type {
+		MessageData::<T::OutboundMessageFee>::type_info()
+	}
+}
+
+impl<T: Config<I>, I: 'static> EncodeLike<StoredMessageData<T, I>>
+	for MessageData<T::OutboundMessageFee>
+{
+}
+
+impl<T: Config<I>, I: 'static> MaxEncodedLen for StoredMessageData<T, I> {
+	fn max_encoded_len() -> usize {
+		T::OutboundMessageFee::max_encoded_len()
+			.saturating_add(T::MaximalOutboundPayloadSize::get() as usize)
+	}
+}
+
 /// Result of messages receival confirmation.
 #[derive(RuntimeDebug, PartialEq, Eq)]
 pub enum ReceivalConfirmationResult {
diff --git a/bridges/modules/messages/src/weights_ext.rs b/bridges/modules/messages/src/weights_ext.rs
index 483a22eda1d6b8cf3079973f74375c666efbfb19..80ac810e8c921c35c58007720453a72fde424735 100644
--- a/bridges/modules/messages/src/weights_ext.rs
+++ b/bridges/modules/messages/src/weights_ext.rs
@@ -199,7 +199,7 @@ pub trait WeightInfoExt: WeightInfo {
 	/// Weight of message send extrinsic.
 	fn send_message_weight(message: &impl Size, db_weight: RuntimeDbWeight) -> Weight {
 		let transaction_overhead = Self::send_message_overhead();
-		let message_size_overhead = Self::send_message_size_overhead(message.size_hint());
+		let message_size_overhead = Self::send_message_size_overhead(message.size());
 		let call_back_overhead = Self::single_message_callback_overhead(db_weight);
 
 		transaction_overhead
@@ -225,7 +225,7 @@ pub trait WeightInfoExt: WeightInfo {
 		let expected_proof_size = EXPECTED_DEFAULT_MESSAGE_LENGTH
 			.saturating_mul(messages_count.saturating_sub(1))
 			.saturating_add(Self::expected_extra_storage_proof_size());
-		let actual_proof_size = proof.size_hint();
+		let actual_proof_size = proof.size();
 		let proof_size_overhead = Self::storage_proof_size_overhead(
 			actual_proof_size.saturating_sub(expected_proof_size),
 		);
@@ -253,7 +253,7 @@ pub trait WeightInfoExt: WeightInfo {
 
 		// proof size overhead weight
 		let expected_proof_size = Self::expected_extra_storage_proof_size();
-		let actual_proof_size = proof.size_hint();
+		let actual_proof_size = proof.size();
 		let proof_size_overhead = Self::storage_proof_size_overhead(
 			actual_proof_size.saturating_sub(expected_proof_size),
 		);
diff --git a/bridges/modules/parachains/src/weights_ext.rs b/bridges/modules/parachains/src/weights_ext.rs
index 3f3815eed96c620aaa724a754fc5f6dbb8d4db6c..f345762dad9c8166500ea0d0872e61847fcdc84b 100644
--- a/bridges/modules/parachains/src/weights_ext.rs
+++ b/bridges/modules/parachains/src/weights_ext.rs
@@ -56,7 +56,7 @@ pub trait WeightInfoExt: WeightInfo {
 		let expected_proof_size = parachains_count
 			.saturating_mul(DEFAULT_PARACHAIN_HEAD_SIZE)
 			.saturating_add(Self::expected_extra_storage_proof_size());
-		let actual_proof_size = proof.size_hint();
+		let actual_proof_size = proof.size();
 		let proof_size_overhead = Self::storage_proof_size_overhead(
 			actual_proof_size.saturating_sub(expected_proof_size),
 		);
diff --git a/bridges/primitives/chain-millau/src/lib.rs b/bridges/primitives/chain-millau/src/lib.rs
index 240c2daaa98c16c1a475620e0554b065b38a2d15..48c80e7017a0801b50a30abe245956695e41c9d4 100644
--- a/bridges/primitives/chain-millau/src/lib.rs
+++ b/bridges/primitives/chain-millau/src/lib.rs
@@ -53,9 +53,6 @@ pub const EXTRA_STORAGE_PROOF_SIZE: u32 = 1024;
 /// Can be computed by subtracting encoded call size from raw transaction size.
 pub const TX_EXTRA_BYTES: u32 = 103;
 
-/// Maximal size (in bytes) of encoded (using `Encode::encode()`) account id.
-pub const MAXIMAL_ENCODED_ACCOUNT_ID_SIZE: u32 = 32;
-
 /// Maximum weight of single Millau block.
 ///
 /// This represents 0.5 seconds of compute assuming a target block time of six seconds.
@@ -363,19 +360,3 @@ sp_api::decl_runtime_apis! {
 		) -> Vec<InboundMessageDetails>;
 	}
 }
-
-#[cfg(test)]
-mod tests {
-	use super::*;
-	use sp_runtime::codec::Encode;
-
-	#[test]
-	fn maximal_account_size_does_not_overflow_constant() {
-		assert!(
-			MAXIMAL_ENCODED_ACCOUNT_ID_SIZE as usize >= AccountId::from([0u8; 32]).encode().len(),
-			"Actual maximal size of encoded AccountId ({}) overflows expected ({})",
-			AccountId::from([0u8; 32]).encode().len(),
-			MAXIMAL_ENCODED_ACCOUNT_ID_SIZE,
-		);
-	}
-}
diff --git a/bridges/primitives/chain-rialto-parachain/src/lib.rs b/bridges/primitives/chain-rialto-parachain/src/lib.rs
index 38b4192bf348c5fe37176d881bf6eda549d81504..9e54744f2d29b38cffa69c0631552bc10839cae6 100644
--- a/bridges/primitives/chain-rialto-parachain/src/lib.rs
+++ b/bridges/primitives/chain-rialto-parachain/src/lib.rs
@@ -50,9 +50,6 @@ pub const EXTRA_STORAGE_PROOF_SIZE: u32 = 1024;
 /// Can be computed by subtracting encoded call size from raw transaction size.
 pub const TX_EXTRA_BYTES: u32 = 104;
 
-/// Maximal size (in bytes) of encoded (using `Encode::encode()`) account id.
-pub const MAXIMAL_ENCODED_ACCOUNT_ID_SIZE: u32 = 32;
-
 /// Maximal weight of single RialtoParachain block.
 ///
 /// This represents two seconds of compute assuming a target block time of six seconds.
@@ -290,19 +287,3 @@ sp_api::decl_runtime_apis! {
 		) -> Vec<InboundMessageDetails>;
 	}
 }
-
-#[cfg(test)]
-mod tests {
-	use super::*;
-	use sp_runtime::codec::Encode;
-
-	#[test]
-	fn maximal_account_size_does_not_overflow_constant() {
-		assert!(
-			MAXIMAL_ENCODED_ACCOUNT_ID_SIZE as usize >= AccountId::from([0u8; 32]).encode().len(),
-			"Actual maximal size of encoded AccountId ({}) overflows expected ({})",
-			AccountId::from([0u8; 32]).encode().len(),
-			MAXIMAL_ENCODED_ACCOUNT_ID_SIZE,
-		);
-	}
-}
diff --git a/bridges/primitives/chain-rialto/src/lib.rs b/bridges/primitives/chain-rialto/src/lib.rs
index e5c5ebab76ff50f459ff11f88a1a4c809dbc83df..9acc9f9f9a6dbc216822e781ee180651372c0cc9 100644
--- a/bridges/primitives/chain-rialto/src/lib.rs
+++ b/bridges/primitives/chain-rialto/src/lib.rs
@@ -44,9 +44,6 @@ pub const EXTRA_STORAGE_PROOF_SIZE: u32 = 1024;
 /// Can be computed by subtracting encoded call size from raw transaction size.
 pub const TX_EXTRA_BYTES: u32 = 104;
 
-/// Maximal size (in bytes) of encoded (using `Encode::encode()`) account id.
-pub const MAXIMAL_ENCODED_ACCOUNT_ID_SIZE: u32 = 32;
-
 /// Maximal weight of single Rialto block.
 ///
 /// This represents two seconds of compute assuming a target block time of six seconds.
@@ -311,19 +308,3 @@ sp_api::decl_runtime_apis! {
 		) -> Vec<InboundMessageDetails>;
 	}
 }
-
-#[cfg(test)]
-mod tests {
-	use super::*;
-	use sp_runtime::codec::Encode;
-
-	#[test]
-	fn maximal_account_size_does_not_overflow_constant() {
-		assert!(
-			MAXIMAL_ENCODED_ACCOUNT_ID_SIZE as usize >= AccountId::from([0u8; 32]).encode().len(),
-			"Actual maximal size of encoded AccountId ({}) overflows expected ({})",
-			AccountId::from([0u8; 32]).encode().len(),
-			MAXIMAL_ENCODED_ACCOUNT_ID_SIZE,
-		);
-	}
-}
diff --git a/bridges/primitives/messages/src/lib.rs b/bridges/primitives/messages/src/lib.rs
index 455caad729b70357b904115eb0b79f114cbfd111..ad1dbd38be9017cbe90d8983173f9b10548ab7a9 100644
--- a/bridges/primitives/messages/src/lib.rs
+++ b/bridges/primitives/messages/src/lib.rs
@@ -21,8 +21,8 @@
 #![allow(clippy::too_many_arguments)]
 
 use bitvec::prelude::*;
-use bp_runtime::messages::DispatchFeePayment;
-use codec::{Decode, Encode};
+use bp_runtime::{messages::DispatchFeePayment, BasicOperatingMode, OperatingMode};
+use codec::{Decode, Encode, MaxEncodedLen};
 use frame_support::RuntimeDebug;
 use scale_info::TypeInfo;
 use sp_std::{collections::vec_deque::VecDeque, prelude::*};
@@ -32,11 +32,10 @@ pub mod storage_keys;
 pub mod target_chain;
 
 // Weight is reexported to avoid additional frame-support dependencies in related crates.
-use bp_runtime::{BasicOperatingMode, OperatingMode};
 pub use frame_support::weights::Weight;
 
 /// Messages pallet operating mode.
-#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo)]
+#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
 #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
 pub enum MessagesOperatingMode {
 	/// Basic operating mode (Normal/Halted)
@@ -89,7 +88,7 @@ pub type BridgeMessageId = (LaneId, MessageNonce);
 pub type MessagePayload = Vec<u8>;
 
 /// Message key (unique message identifier) as it is stored in the storage.
-#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
+#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
 pub struct MessageKey {
 	/// ID of the message lane.
 	pub lane_id: LaneId,
@@ -158,13 +157,13 @@ impl<RelayerId> InboundLaneData<RelayerId> {
 	/// Returns approximate size of the struct, given a number of entries in the `relayers` set and
 	/// size of each entry.
 	///
-	/// Returns `None` if size overflows `u32` limits.
-	pub fn encoded_size_hint(
-		relayer_id_encoded_size: u32,
-		relayers_entries: u32,
-		messages_count: u32,
-	) -> Option<u32> {
-		let message_nonce_size = 8;
+	/// Returns `None` if size overflows `usize` limits.
+	pub fn encoded_size_hint(relayers_entries: usize, messages_count: usize) -> Option<usize>
+	where
+		RelayerId: MaxEncodedLen,
+	{
+		let message_nonce_size = MessageNonce::max_encoded_len();
+		let relayer_id_encoded_size = RelayerId::max_encoded_len();
 		let relayers_entry_size = relayer_id_encoded_size.checked_add(2 * message_nonce_size)?;
 		let relayers_size = relayers_entries.checked_mul(relayers_entry_size)?;
 		let dispatch_results_per_byte = 8;
@@ -305,7 +304,7 @@ pub struct UnrewardedRelayersState {
 }
 
 /// Outbound lane data.
-#[derive(Encode, Decode, Clone, RuntimeDebug, PartialEq, Eq, TypeInfo)]
+#[derive(Encode, Decode, Clone, RuntimeDebug, PartialEq, Eq, TypeInfo, MaxEncodedLen)]
 pub struct OutboundLaneData {
 	/// Nonce of the oldest message that we haven't yet pruned. May point to not-yet-generated
 	/// message if all sent messages are already pruned.
@@ -379,11 +378,8 @@ mod tests {
 			(13u8, 128u8),
 		];
 		for (relayer_entries, messages_count) in test_cases {
-			let expected_size = InboundLaneData::<u8>::encoded_size_hint(
-				1,
-				relayer_entries as _,
-				messages_count as _,
-			);
+			let expected_size =
+				InboundLaneData::<u8>::encoded_size_hint(relayer_entries as _, messages_count as _);
 			let actual_size = InboundLaneData {
 				relayers: (1u8..=relayer_entries)
 					.map(|i| {
diff --git a/bridges/primitives/polkadot-core/src/lib.rs b/bridges/primitives/polkadot-core/src/lib.rs
index 8db1af2fd3367887c411a22a90f0353d7ac7af0b..4b0ef430ad5336b0fdf202be880540c3d51cda61 100644
--- a/bridges/primitives/polkadot-core/src/lib.rs
+++ b/bridges/primitives/polkadot-core/src/lib.rs
@@ -65,11 +65,6 @@ pub mod parachains;
 /// at next runtime upgrade.
 pub const EXTRA_STORAGE_PROOF_SIZE: u32 = 1024;
 
-/// Maximal size (in bytes) of encoded (using `Encode::encode()`) account id.
-///
-/// All polkadot-like chains are using same crypto.
-pub const MAXIMAL_ENCODED_ACCOUNT_ID_SIZE: u32 = 32;
-
 /// All Polkadot-like chains allow normal extrinsics to fill block up to 75 percent.
 ///
 /// This is a copy-paste from the Polkadot repo's `polkadot-runtime-common` crate.
@@ -429,18 +424,6 @@ pub fn account_info_storage_key(id: &AccountId) -> Vec<u8> {
 #[cfg(test)]
 mod tests {
 	use super::*;
-	use sp_runtime::codec::Encode;
-
-	#[test]
-	fn maximal_encoded_account_id_size_is_correct() {
-		let actual_size = AccountId::from([0u8; 32]).encode().len();
-		assert!(
-			actual_size <= MAXIMAL_ENCODED_ACCOUNT_ID_SIZE as usize,
-			"Actual size of encoded account id for Polkadot-like chains ({}) is larger than expected {}",
-			actual_size,
-			MAXIMAL_ENCODED_ACCOUNT_ID_SIZE,
-		);
-	}
 
 	#[test]
 	fn should_generate_storage_key() {
diff --git a/bridges/primitives/polkadot-core/src/parachains.rs b/bridges/primitives/polkadot-core/src/parachains.rs
index af05e7e98582bae16ec217bdf0e108bdeaffa185..59b895065e179c5b16e770fc48b05a201aea7e2a 100644
--- a/bridges/primitives/polkadot-core/src/parachains.rs
+++ b/bridges/primitives/polkadot-core/src/parachains.rs
@@ -93,7 +93,7 @@ pub type ParaHasher = crate::Hasher;
 pub struct ParaHeadsProof(pub Vec<Vec<u8>>);
 
 impl Size for ParaHeadsProof {
-	fn size_hint(&self) -> u32 {
+	fn size(&self) -> u32 {
 		u32::try_from(self.0.iter().fold(0usize, |sum, node| sum.saturating_add(node.len())))
 			.unwrap_or(u32::MAX)
 	}
diff --git a/bridges/primitives/runtime/src/lib.rs b/bridges/primitives/runtime/src/lib.rs
index 4b69bbdb688287be0156bd3c1248790cf4bacc09..2849e346da42658324d0e79833b0f85ece2e06ba 100644
--- a/bridges/primitives/runtime/src/lib.rs
+++ b/bridges/primitives/runtime/src/lib.rs
@@ -18,7 +18,7 @@
 
 #![cfg_attr(not(feature = "std"), no_std)]
 
-use codec::{Decode, Encode, FullCodec};
+use codec::{Decode, Encode, FullCodec, MaxEncodedLen};
 use frame_support::{
 	log, pallet_prelude::DispatchResult, PalletError, RuntimeDebug, StorageHasher, StorageValue,
 };
@@ -139,21 +139,18 @@ pub fn derive_relayer_fund_account_id(bridge_id: ChainId) -> H256 {
 
 /// Anything that has size.
 pub trait Size {
-	/// Return approximate size of this object (in bytes).
-	///
-	/// This function should be lightweight. The result should not necessary be absolutely
-	/// accurate.
-	fn size_hint(&self) -> u32;
+	/// Return size of this object (in bytes).
+	fn size(&self) -> u32;
 }
 
 impl Size for () {
-	fn size_hint(&self) -> u32 {
+	fn size(&self) -> u32 {
 		0
 	}
 }
 
 impl Size for Vec<u8> {
-	fn size_hint(&self) -> u32 {
+	fn size(&self) -> u32 {
 		self.len() as _
 	}
 }
@@ -162,7 +159,7 @@ impl Size for Vec<u8> {
 pub struct PreComputedSize(pub usize);
 
 impl Size for PreComputedSize {
-	fn size_hint(&self) -> u32 {
+	fn size(&self) -> u32 {
 		u32::try_from(self.0).unwrap_or(u32::MAX)
 	}
 }
@@ -308,7 +305,7 @@ pub trait OperatingMode: Send + Copy + Debug + FullCodec {
 }
 
 /// Basic operating modes for a bridges module (Normal/Halted).
-#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo)]
+#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
 #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
 pub enum BasicOperatingMode {
 	/// Normal mode, when all operations are allowed.
diff --git a/bridges/relays/client-kusama/src/lib.rs b/bridges/relays/client-kusama/src/lib.rs
index 8011cbc564666ef3989507f8392b47770aca87e0..31eb3f40e33941050e4890ceada69c4dd7ff7740 100644
--- a/bridges/relays/client-kusama/src/lib.rs
+++ b/bridges/relays/client-kusama/src/lib.rs
@@ -63,7 +63,6 @@ impl Chain for Kusama {
 		bp_kusama::BEST_FINALIZED_KUSAMA_HEADER_METHOD;
 	const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_secs(6);
 	const STORAGE_PROOF_OVERHEAD: u32 = bp_kusama::EXTRA_STORAGE_PROOF_SIZE;
-	const MAXIMAL_ENCODED_ACCOUNT_ID_SIZE: u32 = bp_kusama::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE;
 
 	type SignedBlock = bp_kusama::SignedBlock;
 	type Call = crate::runtime::Call;
diff --git a/bridges/relays/client-millau/src/lib.rs b/bridges/relays/client-millau/src/lib.rs
index 36ad9df69d69b16b713f8cabd5e9639556daae74..4250bd34de75f6f7830679bea9c5bdba317f65b0 100644
--- a/bridges/relays/client-millau/src/lib.rs
+++ b/bridges/relays/client-millau/src/lib.rs
@@ -82,7 +82,6 @@ impl Chain for Millau {
 		bp_millau::BEST_FINALIZED_MILLAU_HEADER_METHOD;
 	const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_secs(5);
 	const STORAGE_PROOF_OVERHEAD: u32 = bp_millau::EXTRA_STORAGE_PROOF_SIZE;
-	const MAXIMAL_ENCODED_ACCOUNT_ID_SIZE: u32 = bp_millau::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE;
 
 	type SignedBlock = millau_runtime::SignedBlock;
 	type Call = millau_runtime::Call;
diff --git a/bridges/relays/client-polkadot/src/lib.rs b/bridges/relays/client-polkadot/src/lib.rs
index 745bbc44fb6fbf67c0f1699ec9d930370c3d62d7..35d876f54638042c0e439c8d7468b6fe1bbd3c96 100644
--- a/bridges/relays/client-polkadot/src/lib.rs
+++ b/bridges/relays/client-polkadot/src/lib.rs
@@ -63,7 +63,6 @@ impl Chain for Polkadot {
 		bp_polkadot::BEST_FINALIZED_POLKADOT_HEADER_METHOD;
 	const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_secs(6);
 	const STORAGE_PROOF_OVERHEAD: u32 = bp_polkadot::EXTRA_STORAGE_PROOF_SIZE;
-	const MAXIMAL_ENCODED_ACCOUNT_ID_SIZE: u32 = bp_polkadot::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE;
 
 	type SignedBlock = bp_polkadot::SignedBlock;
 	type Call = crate::runtime::Call;
diff --git a/bridges/relays/client-rialto-parachain/src/lib.rs b/bridges/relays/client-rialto-parachain/src/lib.rs
index 74393211d93770854b5e5d74734adffe861cbe5c..a6f34201ca5f5ff115cab0764e292bd22410a4b2 100644
--- a/bridges/relays/client-rialto-parachain/src/lib.rs
+++ b/bridges/relays/client-rialto-parachain/src/lib.rs
@@ -63,8 +63,6 @@ impl Chain for RialtoParachain {
 		bp_rialto_parachain::BEST_FINALIZED_RIALTO_PARACHAIN_HEADER_METHOD;
 	const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_secs(5);
 	const STORAGE_PROOF_OVERHEAD: u32 = bp_rialto_parachain::EXTRA_STORAGE_PROOF_SIZE;
-	const MAXIMAL_ENCODED_ACCOUNT_ID_SIZE: u32 =
-		bp_rialto_parachain::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE;
 
 	type SignedBlock = rialto_parachain_runtime::SignedBlock;
 	type Call = rialto_parachain_runtime::Call;
diff --git a/bridges/relays/client-rialto/src/lib.rs b/bridges/relays/client-rialto/src/lib.rs
index 7ddaaabe3611c1397fef4615609598c1ceb02760..67b96563817e3b1485078c6f0c74eb600d998d97 100644
--- a/bridges/relays/client-rialto/src/lib.rs
+++ b/bridges/relays/client-rialto/src/lib.rs
@@ -63,7 +63,6 @@ impl Chain for Rialto {
 		bp_rialto::BEST_FINALIZED_RIALTO_HEADER_METHOD;
 	const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_secs(5);
 	const STORAGE_PROOF_OVERHEAD: u32 = bp_rialto::EXTRA_STORAGE_PROOF_SIZE;
-	const MAXIMAL_ENCODED_ACCOUNT_ID_SIZE: u32 = bp_rialto::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE;
 
 	type SignedBlock = rialto_runtime::SignedBlock;
 	type Call = rialto_runtime::Call;
diff --git a/bridges/relays/client-rococo/src/lib.rs b/bridges/relays/client-rococo/src/lib.rs
index 161b719516ae9dcf523ffd91643c37873c5d9fd9..42a22a8f268bd040bd690d892200afe119b7060a 100644
--- a/bridges/relays/client-rococo/src/lib.rs
+++ b/bridges/relays/client-rococo/src/lib.rs
@@ -66,7 +66,6 @@ impl Chain for Rococo {
 		bp_rococo::BEST_FINALIZED_ROCOCO_HEADER_METHOD;
 	const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_secs(6);
 	const STORAGE_PROOF_OVERHEAD: u32 = bp_rococo::EXTRA_STORAGE_PROOF_SIZE;
-	const MAXIMAL_ENCODED_ACCOUNT_ID_SIZE: u32 = bp_rococo::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE;
 
 	type SignedBlock = bp_rococo::SignedBlock;
 	type Call = crate::runtime::Call;
diff --git a/bridges/relays/client-substrate/src/chain.rs b/bridges/relays/client-substrate/src/chain.rs
index d7f441b73ac2c6ab2b60e514980978364c024e2d..adfecff4eed185abc8e063d97f681905231a3436 100644
--- a/bridges/relays/client-substrate/src/chain.rs
+++ b/bridges/relays/client-substrate/src/chain.rs
@@ -52,8 +52,6 @@ pub trait Chain: ChainBase + Clone {
 	const AVERAGE_BLOCK_INTERVAL: Duration;
 	/// Maximal expected storage proof overhead (in bytes).
 	const STORAGE_PROOF_OVERHEAD: u32;
-	/// Maximal size (in bytes) of SCALE-encoded account id on this chain.
-	const MAXIMAL_ENCODED_ACCOUNT_ID_SIZE: u32;
 
 	/// Block type.
 	type SignedBlock: Member + Serialize + DeserializeOwned + BlockWithJustification<Self::Header>;
diff --git a/bridges/relays/client-substrate/src/test_chain.rs b/bridges/relays/client-substrate/src/test_chain.rs
index f97df643a208d33d1e6954c3865ffab07796d050..f9a9e2455ed99ddd12d8f038853e79dd41897ed4 100644
--- a/bridges/relays/client-substrate/src/test_chain.rs
+++ b/bridges/relays/client-substrate/src/test_chain.rs
@@ -55,7 +55,6 @@ impl Chain for TestChain {
 	const BEST_FINALIZED_HEADER_ID_METHOD: &'static str = "TestMethod";
 	const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_millis(0);
 	const STORAGE_PROOF_OVERHEAD: u32 = 0;
-	const MAXIMAL_ENCODED_ACCOUNT_ID_SIZE: u32 = 0;
 
 	type SignedBlock = sp_runtime::generic::SignedBlock<
 		sp_runtime::generic::Block<Self::Header, sp_runtime::OpaqueExtrinsic>,
diff --git a/bridges/relays/client-westend/src/lib.rs b/bridges/relays/client-westend/src/lib.rs
index cd2e3caab61826749a7177ff04fc285c518d5e09..4b27bfeb82d8ac02e25fe6b092eab173e3811a1c 100644
--- a/bridges/relays/client-westend/src/lib.rs
+++ b/bridges/relays/client-westend/src/lib.rs
@@ -58,7 +58,6 @@ impl Chain for Westend {
 		bp_westend::BEST_FINALIZED_WESTEND_HEADER_METHOD;
 	const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_secs(6);
 	const STORAGE_PROOF_OVERHEAD: u32 = bp_westend::EXTRA_STORAGE_PROOF_SIZE;
-	const MAXIMAL_ENCODED_ACCOUNT_ID_SIZE: u32 = bp_westend::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE;
 
 	type SignedBlock = bp_westend::SignedBlock;
 	type Call = bp_westend::Call;
@@ -117,7 +116,6 @@ impl Chain for Westmint {
 		bp_westend::BEST_FINALIZED_WESTMINT_HEADER_METHOD;
 	const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_secs(6);
 	const STORAGE_PROOF_OVERHEAD: u32 = bp_westend::EXTRA_STORAGE_PROOF_SIZE;
-	const MAXIMAL_ENCODED_ACCOUNT_ID_SIZE: u32 = bp_westend::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE;
 
 	type SignedBlock = bp_westend::SignedBlock;
 	type Call = bp_westend::Call;
diff --git a/bridges/relays/client-wococo/src/lib.rs b/bridges/relays/client-wococo/src/lib.rs
index eb20e40f483e65d359467da13c73cc2815f2ec39..3c96a80b60db57bd7199bc78ef2fe7d713736045 100644
--- a/bridges/relays/client-wococo/src/lib.rs
+++ b/bridges/relays/client-wococo/src/lib.rs
@@ -66,7 +66,6 @@ impl Chain for Wococo {
 		bp_wococo::BEST_FINALIZED_WOCOCO_HEADER_METHOD;
 	const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_secs(6);
 	const STORAGE_PROOF_OVERHEAD: u32 = bp_wococo::EXTRA_STORAGE_PROOF_SIZE;
-	const MAXIMAL_ENCODED_ACCOUNT_ID_SIZE: u32 = bp_wococo::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE;
 
 	type SignedBlock = bp_wococo::SignedBlock;
 	type Call = crate::runtime::Call;
diff --git a/bridges/relays/lib-substrate-relay/src/messages_source.rs b/bridges/relays/lib-substrate-relay/src/messages_source.rs
index 5a6b6554e4bae633e481bf1238afd01638036c6f..8ed8367d35d7cf691a663b2e62360043eb007f85 100644
--- a/bridges/relays/lib-substrate-relay/src/messages_source.rs
+++ b/bridges/relays/lib-substrate-relay/src/messages_source.rs
@@ -467,12 +467,10 @@ where
 /// affect the call weight - we only care about its size.
 fn prepare_dummy_messages_delivery_proof<SC: Chain, TC: Chain>(
 ) -> SubstrateMessagesDeliveryProof<TC> {
-	let single_message_confirmation_size = bp_messages::InboundLaneData::<()>::encoded_size_hint(
-		SC::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE,
-		1,
-		1,
-	)
-	.unwrap_or(u32::MAX);
+	let single_message_confirmation_size =
+		bp_messages::InboundLaneData::<()>::encoded_size_hint(1, 1)
+			.and_then(|x| u32::try_from(x).ok())
+			.unwrap_or(u32::MAX);
 	let proof_size = TC::STORAGE_PROOF_OVERHEAD.saturating_add(single_message_confirmation_size);
 	(
 		UnrewardedRelayersState {
@@ -651,6 +649,7 @@ fn make_message_details_map<C: Chain>(
 mod tests {
 	use super::*;
 	use bp_runtime::messages::DispatchFeePayment;
+	use codec::MaxEncodedLen;
 	use relay_rococo_client::Rococo;
 	use relay_wococo_client::Wococo;
 
@@ -765,7 +764,7 @@ mod tests {
 	#[test]
 	fn prepare_dummy_messages_delivery_proof_works() {
 		let expected_minimal_size =
-			Wococo::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE + Rococo::STORAGE_PROOF_OVERHEAD;
+			bp_wococo::AccountId::max_encoded_len() as u32 + Rococo::STORAGE_PROOF_OVERHEAD;
 		let dummy_proof = prepare_dummy_messages_delivery_proof::<Wococo, Rococo>();
 		assert!(
 			dummy_proof.1.encode().len() as u32 > expected_minimal_size,