diff --git a/bridges/snowbridge/pallets/ethereum-client/src/lib.rs b/bridges/snowbridge/pallets/ethereum-client/src/lib.rs
index 6894977c21f4a2a78b1c7f67eb0b36a791936b92..84b1476931c976bfac8b06202d82affeb4cc0301 100644
--- a/bridges/snowbridge/pallets/ethereum-client/src/lib.rs
+++ b/bridges/snowbridge/pallets/ethereum-client/src/lib.rs
@@ -33,7 +33,10 @@ mod tests;
 mod benchmarking;
 
 use frame_support::{
-	dispatch::DispatchResult, pallet_prelude::OptionQuery, traits::Get, transactional,
+	dispatch::{DispatchResult, PostDispatchInfo},
+	pallet_prelude::OptionQuery,
+	traits::Get,
+	transactional,
 };
 use frame_system::ensure_signed;
 use snowbridge_beacon_primitives::{
@@ -82,6 +85,9 @@ pub mod pallet {
 		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
 		#[pallet::constant]
 		type ForkVersions: Get<ForkVersions>;
+		/// Minimum gap between finalized headers for an update to be free.
+		#[pallet::constant]
+		type FreeHeadersInterval: Get<u32>;
 		type WeightInfo: WeightInfo;
 	}
 
@@ -204,11 +210,10 @@ pub mod pallet {
 		#[transactional]
 		/// Submits a new finalized beacon header update. The update may contain the next
 		/// sync committee.
-		pub fn submit(origin: OriginFor<T>, update: Box<Update>) -> DispatchResult {
+		pub fn submit(origin: OriginFor<T>, update: Box<Update>) -> DispatchResultWithPostInfo {
 			ensure_signed(origin)?;
 			ensure!(!Self::operating_mode().is_halted(), Error::<T>::Halted);
-			Self::process_update(&update)?;
-			Ok(())
+			Self::process_update(&update)
 		}
 
 		/// Halt or resume all pallet operations. May only be called by root.
@@ -280,10 +285,9 @@ pub mod pallet {
 			Ok(())
 		}
 
-		pub(crate) fn process_update(update: &Update) -> DispatchResult {
+		pub(crate) fn process_update(update: &Update) -> DispatchResultWithPostInfo {
 			Self::verify_update(update)?;
-			Self::apply_update(update)?;
-			Ok(())
+			Self::apply_update(update)
 		}
 
 		/// References and strictly follows <https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#validate_light_client_update>
@@ -432,8 +436,9 @@ pub mod pallet {
 		/// Reference and strictly follows <https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#apply_light_client_update
 		/// Applies a finalized beacon header update to the beacon client. If a next sync committee
 		/// is present in the update, verify the sync committee by converting it to a
-		/// SyncCommitteePrepared type. Stores the provided finalized header.
-		fn apply_update(update: &Update) -> DispatchResult {
+		/// SyncCommitteePrepared type. Stores the provided finalized header. Updates are free
+		/// if the certain conditions specified in `check_refundable` are met.
+		fn apply_update(update: &Update) -> DispatchResultWithPostInfo {
 			let latest_finalized_state =
 				FinalizedBeaconState::<T>::get(LatestFinalizedBlockRoot::<T>::get())
 					.ok_or(Error::<T>::NotBootstrapped)?;
@@ -465,11 +470,17 @@ pub mod pallet {
 				});
 			};
 
+			let pays_fee = Self::check_refundable(update, latest_finalized_state.slot);
+			let actual_weight = match update.next_sync_committee_update {
+				None => T::WeightInfo::submit(),
+				Some(_) => T::WeightInfo::submit_with_sync_committee(),
+			};
+
 			if update.finalized_header.slot > latest_finalized_state.slot {
 				Self::store_finalized_header(update.finalized_header, update.block_roots_root)?;
 			}
 
-			Ok(())
+			Ok(PostDispatchInfo { actual_weight: Some(actual_weight), pays_fee })
 		}
 
 		/// Computes the signing root for a given beacon header and domain. The hash tree root
@@ -634,11 +645,31 @@ pub mod pallet {
 				config::SLOTS_PER_EPOCH as u64,
 			));
 			let domain_type = config::DOMAIN_SYNC_COMMITTEE.to_vec();
-			// Domains are used for for seeds, for signatures, and for selecting aggregators.
+			// Domains are used for seeds, for signatures, and for selecting aggregators.
 			let domain = Self::compute_domain(domain_type, fork_version, validators_root)?;
 			// Hash tree root of SigningData - object root + domain
 			let signing_root = Self::compute_signing_root(header, domain)?;
 			Ok(signing_root)
 		}
+
+		/// Updates are free if the update is successful and the interval between the latest
+		/// finalized header in storage and the newly imported header is large enough. All
+		/// successful sync committee updates are free.
+		pub(super) fn check_refundable(update: &Update, latest_slot: u64) -> Pays {
+			// If the sync committee was successfully updated, the update may be free.
+			if update.next_sync_committee_update.is_some() {
+				return Pays::No;
+			}
+
+			// If the latest finalized header is larger than the minimum slot interval, the header
+			// import transaction is free.
+			if update.finalized_header.slot >=
+				latest_slot.saturating_add(T::FreeHeadersInterval::get() as u64)
+			{
+				return Pays::No;
+			}
+
+			Pays::Yes
+		}
 	}
 }
diff --git a/bridges/snowbridge/pallets/ethereum-client/src/mock.rs b/bridges/snowbridge/pallets/ethereum-client/src/mock.rs
index 96298d4fa8962641d6ec8bab0ef67b751ab749cc..be456565d407a944eb853ec9bd2921f3d81a3862 100644
--- a/bridges/snowbridge/pallets/ethereum-client/src/mock.rs
+++ b/bridges/snowbridge/pallets/ethereum-client/src/mock.rs
@@ -10,6 +10,7 @@ use sp_std::default::Default;
 use std::{fs::File, path::PathBuf};
 
 type Block = frame_system::mocking::MockBlock<Test>;
+use frame_support::traits::ConstU32;
 use sp_runtime::BuildStorage;
 
 fn load_fixture<T>(basename: String) -> Result<T, serde_json::Error>
@@ -108,9 +109,12 @@ parameter_types! {
 	};
 }
 
+pub const FREE_SLOTS_INTERVAL: u32 = config::SLOTS_PER_EPOCH as u32;
+
 impl ethereum_beacon_client::Config for Test {
 	type RuntimeEvent = RuntimeEvent;
 	type ForkVersions = ChainForkVersions;
+	type FreeHeadersInterval = ConstU32<FREE_SLOTS_INTERVAL>;
 	type WeightInfo = ();
 }
 
diff --git a/bridges/snowbridge/pallets/ethereum-client/src/tests.rs b/bridges/snowbridge/pallets/ethereum-client/src/tests.rs
index c16743b75ea40b793947240773d159da908dab01..82a3b8224470c12ecdd8243f9d7053adc824e6d4 100644
--- a/bridges/snowbridge/pallets/ethereum-client/src/tests.rs
+++ b/bridges/snowbridge/pallets/ethereum-client/src/tests.rs
@@ -1,21 +1,20 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
 use crate::{
-	functions::compute_period, sync_committee_sum, verify_merkle_branch, BeaconHeader,
-	CompactBeaconState, Error, FinalizedBeaconState, LatestFinalizedBlockRoot, NextSyncCommittee,
-	SyncCommitteePrepared,
-};
-
-use crate::mock::{
-	get_message_verification_payload, load_checkpoint_update_fixture,
-	load_finalized_header_update_fixture, load_next_finalized_header_update_fixture,
-	load_next_sync_committee_update_fixture, load_sync_committee_update_fixture,
+	functions::compute_period,
+	mock::{
+		get_message_verification_payload, load_checkpoint_update_fixture,
+		load_finalized_header_update_fixture, load_next_finalized_header_update_fixture,
+		load_next_sync_committee_update_fixture, load_sync_committee_update_fixture,
+	},
+	sync_committee_sum, verify_merkle_branch, BeaconHeader, CompactBeaconState, Error,
+	FinalizedBeaconState, LatestFinalizedBlockRoot, NextSyncCommittee, SyncCommitteePrepared,
 };
 
 pub use crate::mock::*;
 
 use crate::config::{EPOCHS_PER_SYNC_COMMITTEE_PERIOD, SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT};
-use frame_support::{assert_err, assert_noop, assert_ok};
+use frame_support::{assert_err, assert_noop, assert_ok, pallet_prelude::Pays};
 use hex_literal::hex;
 use snowbridge_beacon_primitives::{
 	types::deneb, Fork, ForkVersions, NextSyncCommitteeUpdate, VersionedExecutionPayloadHeader,
@@ -129,6 +128,39 @@ pub fn compute_domain_bls() {
 	});
 }
 
+#[test]
+pub fn may_refund_call_fee() {
+	let finalized_update = Box::new(load_next_finalized_header_update_fixture());
+	let sync_committee_update = Box::new(load_sync_committee_update_fixture());
+	new_tester().execute_with(|| {
+		let free_headers_interval: u64 = crate::mock::FREE_SLOTS_INTERVAL as u64;
+		// Not free, smaller than the allowed free header interval
+		assert_eq!(
+			EthereumBeaconClient::check_refundable(
+				&finalized_update.clone(),
+				finalized_update.finalized_header.slot + free_headers_interval
+			),
+			Pays::Yes
+		);
+		// Is free, larger than the minimum interval
+		assert_eq!(
+			EthereumBeaconClient::check_refundable(
+				&finalized_update,
+				finalized_update.finalized_header.slot - (free_headers_interval + 2)
+			),
+			Pays::No
+		);
+		// Is free, valid sync committee update
+		assert_eq!(
+			EthereumBeaconClient::check_refundable(
+				&sync_committee_update,
+				finalized_update.finalized_header.slot
+			),
+			Pays::No
+		);
+	});
+}
+
 #[test]
 pub fn verify_merkle_branch_for_finalized_root() {
 	new_tester().execute_with(|| {
@@ -340,7 +372,9 @@ fn submit_update_in_current_period() {
 
 	new_tester().execute_with(|| {
 		assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
-		assert_ok!(EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update.clone()));
+		let result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update.clone());
+		assert_ok!(result);
+		assert_eq!(result.unwrap().pays_fee, Pays::Yes);
 		let block_root: H256 = update.finalized_header.hash_tree_root().unwrap();
 		assert!(<FinalizedBeaconState<Test>>::contains_key(block_root));
 	});
@@ -357,7 +391,9 @@ fn submit_update_with_sync_committee_in_current_period() {
 	new_tester().execute_with(|| {
 		assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
 		assert!(!<NextSyncCommittee<Test>>::exists());
-		assert_ok!(EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update));
+		let result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update);
+		assert_ok!(result);
+		assert_eq!(result.unwrap().pays_fee, Pays::No);
 		assert!(<NextSyncCommittee<Test>>::exists());
 	});
 }
@@ -374,20 +410,21 @@ fn reject_submit_update_in_next_period() {
 
 	new_tester().execute_with(|| {
 		assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
-		assert_ok!(EthereumBeaconClient::submit(
-			RuntimeOrigin::signed(1),
-			sync_committee_update.clone()
-		));
+		let result =
+			EthereumBeaconClient::submit(RuntimeOrigin::signed(1), sync_committee_update.clone());
+		assert_ok!(result);
+		assert_eq!(result.unwrap().pays_fee, Pays::No);
+
 		// check an update in the next period is rejected
-		assert_err!(
-			EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update.clone()),
-			Error::<Test>::SyncCommitteeUpdateRequired
-		);
+		let second_result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update.clone());
+		assert_err!(second_result, Error::<Test>::SyncCommitteeUpdateRequired);
+		assert_eq!(second_result.unwrap_err().post_info.pays_fee, Pays::Yes);
+
 		// submit update with next sync committee
-		assert_ok!(EthereumBeaconClient::submit(
-			RuntimeOrigin::signed(1),
-			next_sync_committee_update
-		));
+		let third_result =
+			EthereumBeaconClient::submit(RuntimeOrigin::signed(1), next_sync_committee_update);
+		assert_ok!(third_result);
+		assert_eq!(third_result.unwrap().pays_fee, Pays::No);
 		// check same header in the next period can now be submitted successfully
 		assert_ok!(EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update.clone()));
 		let block_root: H256 = update.finalized_header.clone().hash_tree_root().unwrap();
@@ -407,10 +444,9 @@ fn submit_update_with_invalid_header_proof() {
 	new_tester().execute_with(|| {
 		assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
 		assert!(!<NextSyncCommittee<Test>>::exists());
-		assert_err!(
-			EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update),
-			Error::<Test>::InvalidHeaderMerkleProof
-		);
+		let result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update);
+		assert_err!(result, Error::<Test>::InvalidHeaderMerkleProof);
+		assert_eq!(result.unwrap_err().post_info.pays_fee, Pays::Yes);
 	});
 }
 
@@ -426,10 +462,9 @@ fn submit_update_with_invalid_block_roots_proof() {
 	new_tester().execute_with(|| {
 		assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
 		assert!(!<NextSyncCommittee<Test>>::exists());
-		assert_err!(
-			EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update),
-			Error::<Test>::InvalidBlockRootsRootMerkleProof
-		);
+		let result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update);
+		assert_err!(result, Error::<Test>::InvalidBlockRootsRootMerkleProof);
+		assert_eq!(result.unwrap_err().post_info.pays_fee, Pays::Yes);
 	});
 }
 
@@ -447,10 +482,9 @@ fn submit_update_with_invalid_next_sync_committee_proof() {
 	new_tester().execute_with(|| {
 		assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
 		assert!(!<NextSyncCommittee<Test>>::exists());
-		assert_err!(
-			EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update),
-			Error::<Test>::InvalidSyncCommitteeMerkleProof
-		);
+		let result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update);
+		assert_err!(result, Error::<Test>::InvalidSyncCommitteeMerkleProof);
+		assert_eq!(result.unwrap_err().post_info.pays_fee, Pays::Yes);
 	});
 }
 
@@ -464,14 +498,14 @@ fn submit_update_with_skipped_period() {
 
 	new_tester().execute_with(|| {
 		assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
-		assert_ok!(EthereumBeaconClient::submit(
-			RuntimeOrigin::signed(1),
-			sync_committee_update.clone()
-		));
-		assert_err!(
-			EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update),
-			Error::<Test>::SkippedSyncCommitteePeriod
-		);
+		let result =
+			EthereumBeaconClient::submit(RuntimeOrigin::signed(1), sync_committee_update.clone());
+		assert_ok!(result);
+		assert_eq!(result.unwrap().pays_fee, Pays::No);
+
+		let second_result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update);
+		assert_err!(second_result, Error::<Test>::SkippedSyncCommitteePeriod);
+		assert_eq!(second_result.unwrap_err().post_info.pays_fee, Pays::Yes);
 	});
 }
 
@@ -487,9 +521,16 @@ fn submit_update_with_sync_committee_in_next_period() {
 	new_tester().execute_with(|| {
 		assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
 		assert!(!<NextSyncCommittee<Test>>::exists());
-		assert_ok!(EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update.clone()));
+
+		let result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update.clone());
+		assert_ok!(result);
+		assert_eq!(result.unwrap().pays_fee, Pays::No);
 		assert!(<NextSyncCommittee<Test>>::exists());
-		assert_ok!(EthereumBeaconClient::submit(RuntimeOrigin::signed(1), next_update.clone()));
+
+		let second_result =
+			EthereumBeaconClient::submit(RuntimeOrigin::signed(1), next_update.clone());
+		assert_ok!(second_result);
+		assert_eq!(second_result.unwrap().pays_fee, Pays::No);
 		let last_finalized_state =
 			FinalizedBeaconState::<Test>::get(LatestFinalizedBlockRoot::<Test>::get()).unwrap();
 		let last_synced_period = compute_period(last_finalized_state.slot);
@@ -505,13 +546,12 @@ fn submit_update_with_sync_committee_invalid_signature_slot() {
 	new_tester().execute_with(|| {
 		assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
 
-		// makes a invalid update with signature_slot should be more than attested_slot
+		// makes an invalid update with signature_slot should be more than attested_slot
 		update.signature_slot = update.attested_header.slot;
 
-		assert_err!(
-			EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update),
-			Error::<Test>::InvalidUpdateSlot
-		);
+		let result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update);
+		assert_err!(result, Error::<Test>::InvalidUpdateSlot);
+		assert_eq!(result.unwrap_err().post_info.pays_fee, Pays::Yes);
 	});
 }
 
@@ -525,10 +565,9 @@ fn submit_update_with_skipped_sync_committee_period() {
 
 	new_tester().execute_with(|| {
 		assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
-		assert_err!(
-			EthereumBeaconClient::submit(RuntimeOrigin::signed(1), finalized_update),
-			Error::<Test>::SkippedSyncCommitteePeriod
-		);
+		let result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), finalized_update);
+		assert_err!(result, Error::<Test>::SkippedSyncCommitteePeriod);
+		assert_eq!(result.unwrap_err().post_info.pays_fee, Pays::Yes);
 	});
 }
 
@@ -546,10 +585,9 @@ fn submit_irrelevant_update() {
 		update.attested_header.slot = checkpoint.header.slot;
 		update.signature_slot = checkpoint.header.slot + 1;
 
-		assert_err!(
-			EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update),
-			Error::<Test>::IrrelevantUpdate
-		);
+		let result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update);
+		assert_err!(result, Error::<Test>::IrrelevantUpdate);
+		assert_eq!(result.unwrap_err().post_info.pays_fee, Pays::Yes);
 	});
 }
 
@@ -558,10 +596,9 @@ fn submit_update_with_missing_bootstrap() {
 	let update = Box::new(load_next_finalized_header_update_fixture());
 
 	new_tester().execute_with(|| {
-		assert_err!(
-			EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update),
-			Error::<Test>::NotBootstrapped
-		);
+		let result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update);
+		assert_err!(result, Error::<Test>::NotBootstrapped);
+		assert_eq!(result.unwrap_err().post_info.pays_fee, Pays::Yes);
 	});
 }
 
@@ -574,7 +611,9 @@ fn submit_update_with_invalid_sync_committee_update() {
 	new_tester().execute_with(|| {
 		assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
 
-		assert_ok!(EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update));
+		let result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update);
+		assert_ok!(result);
+		assert_eq!(result.unwrap().pays_fee, Pays::No);
 
 		// makes update with invalid next_sync_committee
 		<FinalizedBeaconState<Test>>::mutate(<LatestFinalizedBlockRoot<Test>>::get(), |x| {
@@ -586,10 +625,9 @@ fn submit_update_with_invalid_sync_committee_update() {
 		let next_sync_committee = NextSyncCommitteeUpdate::default();
 		next_update.next_sync_committee_update = Some(next_sync_committee);
 
-		assert_err!(
-			EthereumBeaconClient::submit(RuntimeOrigin::signed(1), next_update),
-			Error::<Test>::InvalidSyncCommitteeUpdate
-		);
+		let second_result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), next_update);
+		assert_err!(second_result, Error::<Test>::InvalidSyncCommitteeUpdate);
+		assert_eq!(second_result.unwrap_err().post_info.pays_fee, Pays::Yes);
 	});
 }
 
@@ -612,12 +650,15 @@ fn submit_finalized_header_update_with_too_large_gap() {
 
 	new_tester().execute_with(|| {
 		assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
-		assert_ok!(EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update.clone()));
+		let result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update.clone());
+		assert_ok!(result);
+		assert_eq!(result.unwrap().pays_fee, Pays::No);
 		assert!(<NextSyncCommittee<Test>>::exists());
-		assert_err!(
-			EthereumBeaconClient::submit(RuntimeOrigin::signed(1), next_update.clone()),
-			Error::<Test>::InvalidFinalizedHeaderGap
-		);
+
+		let second_result =
+			EthereumBeaconClient::submit(RuntimeOrigin::signed(1), next_update.clone());
+		assert_err!(second_result, Error::<Test>::InvalidFinalizedHeaderGap);
+		assert_eq!(second_result.unwrap_err().post_info.pays_fee, Pays::Yes);
 	});
 }
 
@@ -637,14 +678,41 @@ fn submit_finalized_header_update_with_gap_at_limit() {
 
 	new_tester().execute_with(|| {
 		assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
-		assert_ok!(EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update.clone()));
+
+		let result = EthereumBeaconClient::submit(RuntimeOrigin::signed(1), update.clone());
+		assert_ok!(result);
+		assert_eq!(result.unwrap().pays_fee, Pays::No);
 		assert!(<NextSyncCommittee<Test>>::exists());
+
+		let second_result =
+			EthereumBeaconClient::submit(RuntimeOrigin::signed(1), next_update.clone());
 		assert_err!(
-			EthereumBeaconClient::submit(RuntimeOrigin::signed(1), next_update.clone()),
+			second_result,
 			// The test should pass the InvalidFinalizedHeaderGap check, and will fail at the
 			// next check, the merkle proof, because we changed the next_update slots.
 			Error::<Test>::InvalidHeaderMerkleProof
 		);
+		assert_eq!(second_result.unwrap_err().post_info.pays_fee, Pays::Yes);
+	});
+}
+
+#[test]
+fn duplicate_sync_committee_updates_are_not_free() {
+	let checkpoint = Box::new(load_checkpoint_update_fixture());
+	let sync_committee_update = Box::new(load_sync_committee_update_fixture());
+
+	new_tester().execute_with(|| {
+		assert_ok!(EthereumBeaconClient::process_checkpoint_update(&checkpoint));
+		let result =
+			EthereumBeaconClient::submit(RuntimeOrigin::signed(1), sync_committee_update.clone());
+		assert_ok!(result);
+		assert_eq!(result.unwrap().pays_fee, Pays::No);
+
+		// Check that if the same update is submitted, the update is not free.
+		let second_result =
+			EthereumBeaconClient::submit(RuntimeOrigin::signed(1), sync_committee_update);
+		assert_err!(second_result, Error::<Test>::IrrelevantUpdate);
+		assert_eq!(second_result.unwrap_err().post_info.pays_fee, Pays::Yes);
 	});
 }
 
diff --git a/bridges/snowbridge/pallets/inbound-queue/src/mock.rs b/bridges/snowbridge/pallets/inbound-queue/src/mock.rs
index a031676c6076ad417a9d0fd1f060e9387bb99eea..871df6d1e51b333c71167295f0519a9c4e146b7b 100644
--- a/bridges/snowbridge/pallets/inbound-queue/src/mock.rs
+++ b/bridges/snowbridge/pallets/inbound-queue/src/mock.rs
@@ -88,6 +88,7 @@ parameter_types! {
 impl snowbridge_pallet_ethereum_client::Config for Test {
 	type RuntimeEvent = RuntimeEvent;
 	type ForkVersions = ChainForkVersions;
+	type FreeHeadersInterval = ConstU32<32>;
 	type WeightInfo = ();
 }
 
diff --git a/bridges/snowbridge/runtime/test-common/src/lib.rs b/bridges/snowbridge/runtime/test-common/src/lib.rs
index 8f36313e360f1a41d85b54b9796e62a45f42e9b4..b157ad4356bdf5769119dd52b0fe027d72fab2e1 100644
--- a/bridges/snowbridge/runtime/test-common/src/lib.rs
+++ b/bridges/snowbridge/runtime/test-common/src/lib.rs
@@ -12,7 +12,7 @@ use parachains_runtimes_test_utils::{
 };
 use snowbridge_core::{ChannelId, ParaId};
 use snowbridge_pallet_ethereum_client_fixtures::*;
-use sp_core::{H160, U256};
+use sp_core::{Get, H160, U256};
 use sp_keyring::AccountKeyring::*;
 use sp_runtime::{traits::Header, AccountId32, DigestItem, SaturatedConversion, Saturating};
 use xcm::{
@@ -466,23 +466,37 @@ pub fn ethereum_extrinsic<Runtime>(
 			let initial_checkpoint = make_checkpoint();
 			let update = make_finalized_header_update();
 			let sync_committee_update = make_sync_committee_update();
+			let mut invalid_update = make_finalized_header_update();
+			let mut invalid_sync_committee_update = make_sync_committee_update();
+			invalid_update.finalized_header.slot = 4354;
+			invalid_sync_committee_update.finalized_header.slot = 4354;
 
 			let alice = Alice;
 			let alice_account = alice.to_account_id();
 			<pallet_balances::Pallet<Runtime>>::mint_into(
-				&alice_account.into(),
+				&alice_account.clone().into(),
 				10_000_000_000_000_u128.saturated_into::<BalanceOf<Runtime>>(),
 			)
 			.unwrap();
+			let balance_before =
+				<pallet_balances::Pallet<Runtime>>::free_balance(&alice_account.clone().into());
 
 			assert_ok!(<snowbridge_pallet_ethereum_client::Pallet<Runtime>>::force_checkpoint(
 				RuntimeHelper::<Runtime>::root_origin(),
-				initial_checkpoint,
+				initial_checkpoint.clone(),
 			));
+			let balance_after_checkpoint =
+				<pallet_balances::Pallet<Runtime>>::free_balance(&alice_account.clone().into());
 
 			let update_call: <Runtime as pallet_utility::Config>::RuntimeCall =
 				snowbridge_pallet_ethereum_client::Call::<Runtime>::submit {
-					update: Box::new(*update),
+					update: Box::new(*update.clone()),
+				}
+				.into();
+
+			let invalid_update_call: <Runtime as pallet_utility::Config>::RuntimeCall =
+				snowbridge_pallet_ethereum_client::Call::<Runtime>::submit {
+					update: Box::new(*invalid_update),
 				}
 				.into();
 
@@ -492,12 +506,63 @@ pub fn ethereum_extrinsic<Runtime>(
 				}
 				.into();
 
+			let invalid_update_sync_committee_call: <Runtime as pallet_utility::Config>::RuntimeCall =
+				snowbridge_pallet_ethereum_client::Call::<Runtime>::submit {
+					update: Box::new(*invalid_sync_committee_update),
+				}
+					.into();
+
+			// Finalized header update
 			let update_outcome = construct_and_apply_extrinsic(alice, update_call.into());
 			assert_ok!(update_outcome);
+			let balance_after_update =
+				<pallet_balances::Pallet<Runtime>>::free_balance(&alice_account.clone().into());
+
+			// Invalid finalized header update
+			let invalid_update_outcome =
+				construct_and_apply_extrinsic(alice, invalid_update_call.into());
+			assert_err!(
+				invalid_update_outcome,
+				snowbridge_pallet_ethereum_client::Error::<Runtime>::InvalidUpdateSlot
+			);
+			let balance_after_invalid_update =
+				<pallet_balances::Pallet<Runtime>>::free_balance(&alice_account.clone().into());
 
+			// Sync committee update
 			let sync_committee_outcome =
 				construct_and_apply_extrinsic(alice, update_sync_committee_call.into());
 			assert_ok!(sync_committee_outcome);
+			let balance_after_sync_com_update =
+				<pallet_balances::Pallet<Runtime>>::free_balance(&alice_account.clone().into());
+
+			// Invalid sync committee update
+			let invalid_sync_committee_outcome =
+				construct_and_apply_extrinsic(alice, invalid_update_sync_committee_call.into());
+			assert_err!(
+				invalid_sync_committee_outcome,
+				snowbridge_pallet_ethereum_client::Error::<Runtime>::InvalidUpdateSlot
+			);
+			let balance_after_invalid_sync_com_update =
+				<pallet_balances::Pallet<Runtime>>::free_balance(&alice_account.clone().into());
+
+			// Assert paid operations are charged and free operations are free
+			// Checkpoint is a free operation
+			assert!(balance_before == balance_after_checkpoint);
+			let gap =
+				<Runtime as snowbridge_pallet_ethereum_client::Config>::FreeHeadersInterval::get();
+			// Large enough header gap is free
+			if update.finalized_header.slot >= initial_checkpoint.header.slot + gap as u64 {
+				assert!(balance_after_checkpoint == balance_after_update);
+			} else {
+				// Otherwise paid
+				assert!(balance_after_checkpoint > balance_after_update);
+			}
+			// An invalid update is paid
+			assert!(balance_after_update > balance_after_invalid_update);
+			// A successful sync committee update is free
+			assert!(balance_after_invalid_update == balance_after_sync_com_update);
+			// An invalid sync committee update is paid
+			assert!(balance_after_sync_com_update > balance_after_invalid_sync_com_update);
 		});
 }
 
diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs
index 01a762d4b99f94f39e7a6050ff13a720002063bd..6c0486c62fa658e6020616a566be1d4ad5b87f1d 100644
--- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs
+++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs
@@ -14,9 +14,34 @@
 // You should have received a copy of the GNU General Public License
 // along with Cumulus.  If not, see <http://www.gnu.org/licenses/>.
 
-use crate::{xcm_config::UniversalLocation, Runtime};
-use snowbridge_router_primitives::outbound::EthereumBlobExporter;
-use testnet_parachains_constants::rococo::snowbridge::EthereumNetwork;
+#[cfg(not(feature = "runtime-benchmarks"))]
+use crate::XcmRouter;
+use crate::{
+	xcm_config, xcm_config::UniversalLocation, Balances, EthereumInboundQueue,
+	EthereumOutboundQueue, EthereumSystem, MessageQueue, Runtime, RuntimeEvent, TransactionByteFee,
+	TreasuryAccount,
+};
+use parachains_common::{AccountId, Balance};
+use snowbridge_beacon_primitives::{Fork, ForkVersions};
+use snowbridge_core::{gwei, meth, AllowSiblingsOnly, PricingParameters, Rewards};
+use snowbridge_router_primitives::{inbound::MessageToXcm, outbound::EthereumBlobExporter};
+use sp_core::H160;
+use testnet_parachains_constants::rococo::{
+	currency::*,
+	fee::WeightToFee,
+	snowbridge::{EthereumNetwork, INBOUND_QUEUE_PALLET_INDEX},
+};
+
+#[cfg(feature = "runtime-benchmarks")]
+use benchmark_helpers::DoNothingRouter;
+use frame_support::{parameter_types, weights::ConstantMultiplier};
+use pallet_xcm::EnsureXcm;
+use sp_runtime::{
+	traits::{ConstU32, ConstU8, Keccak256},
+	FixedU128,
+};
+
+pub const SLOTS_PER_EPOCH: u32 = snowbridge_pallet_ethereum_client::config::SLOTS_PER_EPOCH as u32;
 
 /// Exports message to the Ethereum Gateway contract.
 pub type SnowbridgeExporter = EthereumBlobExporter<
@@ -25,3 +50,173 @@ pub type SnowbridgeExporter = EthereumBlobExporter<
 	snowbridge_pallet_outbound_queue::Pallet<Runtime>,
 	snowbridge_core::AgentIdOf,
 >;
+
+// Ethereum Bridge
+parameter_types! {
+	pub storage EthereumGatewayAddress: H160 = H160(hex_literal::hex!("EDa338E4dC46038493b885327842fD3E301CaB39"));
+}
+
+parameter_types! {
+	pub const CreateAssetCall: [u8;2] = [53, 0];
+	pub const CreateAssetDeposit: u128 = (UNITS / 10) + EXISTENTIAL_DEPOSIT;
+	pub Parameters: PricingParameters<u128> = PricingParameters {
+		exchange_rate: FixedU128::from_rational(1, 400),
+		fee_per_gas: gwei(20),
+		rewards: Rewards { local: 1 * UNITS, remote: meth(1) },
+		multiplier: FixedU128::from_rational(1, 1),
+	};
+}
+
+impl snowbridge_pallet_inbound_queue::Config for Runtime {
+	type RuntimeEvent = RuntimeEvent;
+	type Verifier = snowbridge_pallet_ethereum_client::Pallet<Runtime>;
+	type Token = Balances;
+	#[cfg(not(feature = "runtime-benchmarks"))]
+	type XcmSender = XcmRouter;
+	#[cfg(feature = "runtime-benchmarks")]
+	type XcmSender = DoNothingRouter;
+	type ChannelLookup = EthereumSystem;
+	type GatewayAddress = EthereumGatewayAddress;
+	#[cfg(feature = "runtime-benchmarks")]
+	type Helper = Runtime;
+	type MessageConverter = MessageToXcm<
+		CreateAssetCall,
+		CreateAssetDeposit,
+		ConstU8<INBOUND_QUEUE_PALLET_INDEX>,
+		AccountId,
+		Balance,
+	>;
+	type WeightToFee = WeightToFee;
+	type LengthToFee = ConstantMultiplier<Balance, TransactionByteFee>;
+	type MaxMessageSize = ConstU32<2048>;
+	type WeightInfo = crate::weights::snowbridge_pallet_inbound_queue::WeightInfo<Runtime>;
+	type PricingParameters = EthereumSystem;
+	type AssetTransactor = <xcm_config::XcmConfig as xcm_executor::Config>::AssetTransactor;
+}
+
+impl snowbridge_pallet_outbound_queue::Config for Runtime {
+	type RuntimeEvent = RuntimeEvent;
+	type Hashing = Keccak256;
+	type MessageQueue = MessageQueue;
+	type Decimals = ConstU8<12>;
+	type MaxMessagePayloadSize = ConstU32<2048>;
+	type MaxMessagesPerBlock = ConstU32<32>;
+	type GasMeter = snowbridge_core::outbound::ConstantGasMeter;
+	type Balance = Balance;
+	type WeightToFee = WeightToFee;
+	type WeightInfo = crate::weights::snowbridge_pallet_outbound_queue::WeightInfo<Runtime>;
+	type PricingParameters = EthereumSystem;
+	type Channels = EthereumSystem;
+}
+
+#[cfg(any(feature = "std", feature = "fast-runtime", feature = "runtime-benchmarks", test))]
+parameter_types! {
+	pub const ChainForkVersions: ForkVersions = ForkVersions {
+		genesis: Fork {
+			version: [0, 0, 0, 0], // 0x00000000
+			epoch: 0,
+		},
+		altair: Fork {
+			version: [1, 0, 0, 0], // 0x01000000
+			epoch: 0,
+		},
+		bellatrix: Fork {
+			version: [2, 0, 0, 0], // 0x02000000
+			epoch: 0,
+		},
+		capella: Fork {
+			version: [3, 0, 0, 0], // 0x03000000
+			epoch: 0,
+		},
+		deneb: Fork {
+			version: [4, 0, 0, 0], // 0x04000000
+			epoch: 0,
+		}
+	};
+}
+
+#[cfg(not(any(feature = "std", feature = "fast-runtime", feature = "runtime-benchmarks", test)))]
+parameter_types! {
+	pub const ChainForkVersions: ForkVersions = ForkVersions {
+		genesis: Fork {
+			version: [144, 0, 0, 111], // 0x90000069
+			epoch: 0,
+		},
+		altair: Fork {
+			version: [144, 0, 0, 112], // 0x90000070
+			epoch: 50,
+		},
+		bellatrix: Fork {
+			version: [144, 0, 0, 113], // 0x90000071
+			epoch: 100,
+		},
+		capella: Fork {
+			version: [144, 0, 0, 114], // 0x90000072
+			epoch: 56832,
+		},
+		deneb: Fork {
+			version: [144, 0, 0, 115], // 0x90000073
+			epoch: 132608,
+		},
+	};
+}
+
+impl snowbridge_pallet_ethereum_client::Config for Runtime {
+	type RuntimeEvent = RuntimeEvent;
+	type ForkVersions = ChainForkVersions;
+	// Free consensus update every epoch. Works out to be 225 updates per day.
+	type FreeHeadersInterval = ConstU32<SLOTS_PER_EPOCH>;
+	type WeightInfo = crate::weights::snowbridge_pallet_ethereum_client::WeightInfo<Runtime>;
+}
+
+impl snowbridge_pallet_system::Config for Runtime {
+	type RuntimeEvent = RuntimeEvent;
+	type OutboundQueue = EthereumOutboundQueue;
+	type SiblingOrigin = EnsureXcm<AllowSiblingsOnly>;
+	type AgentIdOf = snowbridge_core::AgentIdOf;
+	type TreasuryAccount = TreasuryAccount;
+	type Token = Balances;
+	type WeightInfo = crate::weights::snowbridge_pallet_system::WeightInfo<Runtime>;
+	#[cfg(feature = "runtime-benchmarks")]
+	type Helper = ();
+	type DefaultPricingParameters = Parameters;
+	type InboundDeliveryCost = EthereumInboundQueue;
+}
+
+#[cfg(feature = "runtime-benchmarks")]
+pub mod benchmark_helpers {
+	use crate::{EthereumBeaconClient, Runtime, RuntimeOrigin};
+	use codec::Encode;
+	use snowbridge_beacon_primitives::BeaconHeader;
+	use snowbridge_pallet_inbound_queue::BenchmarkHelper;
+	use sp_core::H256;
+	use xcm::latest::{Assets, Location, SendError, SendResult, SendXcm, Xcm, XcmHash};
+
+	impl<T: snowbridge_pallet_ethereum_client::Config> BenchmarkHelper<T> for Runtime {
+		fn initialize_storage(beacon_header: BeaconHeader, block_roots_root: H256) {
+			EthereumBeaconClient::store_finalized_header(beacon_header, block_roots_root).unwrap();
+		}
+	}
+
+	pub struct DoNothingRouter;
+	impl SendXcm for DoNothingRouter {
+		type Ticket = Xcm<()>;
+
+		fn validate(
+			_dest: &mut Option<Location>,
+			xcm: &mut Option<Xcm<()>>,
+		) -> SendResult<Self::Ticket> {
+			Ok((xcm.clone().unwrap(), Assets::new()))
+		}
+		fn deliver(xcm: Xcm<()>) -> Result<XcmHash, SendError> {
+			let hash = xcm.using_encoded(sp_io::hashing::blake2_256);
+			Ok(hash)
+		}
+	}
+
+	impl snowbridge_pallet_system::BenchmarkHelper<RuntimeOrigin> for () {
+		fn make_xcm_origin(location: Location) -> RuntimeOrigin {
+			RuntimeOrigin::from(pallet_xcm::Origin::Xcm(location))
+		}
+	}
+}
diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/lib.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/lib.rs
index 100bff5a0705dce79e927ea5af1284670ee81a0e..14409ce4642d3cb04bf2ea3f4c6166d6dd1632e1 100644
--- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/lib.rs
+++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/lib.rs
@@ -42,20 +42,13 @@ use bridge_runtime_common::extensions::{
 	CheckAndBoostBridgeGrandpaTransactions, CheckAndBoostBridgeParachainsTransactions,
 };
 use cumulus_pallet_parachain_system::RelayNumberMonotonicallyIncreases;
-use snowbridge_beacon_primitives::{Fork, ForkVersions};
-use snowbridge_core::{
-	gwei, meth,
-	outbound::{Command, Fee},
-	AgentId, AllowSiblingsOnly, PricingParameters, Rewards,
-};
-use snowbridge_router_primitives::inbound::MessageToXcm;
 use sp_api::impl_runtime_apis;
-use sp_core::{crypto::KeyTypeId, OpaqueMetadata, H160};
+use sp_core::{crypto::KeyTypeId, OpaqueMetadata};
 use sp_runtime::{
 	create_runtime_str, generic, impl_opaque_keys,
-	traits::{Block as BlockT, Keccak256},
+	traits::Block as BlockT,
 	transaction_validity::{TransactionSource, TransactionValidity},
-	ApplyExtrinsicResult, FixedU128,
+	ApplyExtrinsicResult,
 };
 
 #[cfg(feature = "std")]
@@ -76,16 +69,13 @@ use frame_system::{
 	limits::{BlockLength, BlockWeights},
 	EnsureRoot,
 };
-use testnet_parachains_constants::rococo::{
-	consensus::*, currency::*, fee::WeightToFee, snowbridge::INBOUND_QUEUE_PALLET_INDEX, time::*,
-};
+use testnet_parachains_constants::rococo::{consensus::*, currency::*, fee::WeightToFee, time::*};
 
 use bp_runtime::HeaderId;
 use bridge_hub_common::{
 	message_queue::{NarrowOriginToSibling, ParaIdToSibling},
 	AggregateMessageOrigin,
 };
-use pallet_xcm::EnsureXcm;
 pub use sp_consensus_aura::sr25519::AuthorityId as AuraId;
 pub use sp_runtime::{MultiAddress, Perbill, Permill};
 use xcm::VersionedLocation;
@@ -96,7 +86,11 @@ pub use sp_runtime::BuildStorage;
 
 use polkadot_runtime_common::{BlockHashCount, SlowAdjustingFeeUpdate};
 use rococo_runtime_constants::system_parachain::{ASSET_HUB_ID, BRIDGE_HUB_ID};
-use xcm::prelude::*;
+use snowbridge_core::{
+	outbound::{Command, Fee},
+	AgentId, PricingParameters,
+};
+use xcm::{latest::prelude::*, prelude::*};
 use xcm_runtime_apis::{
 	dry_run::{CallDryRunEffects, Error as XcmDryRunApiError, XcmDryRunEffects},
 	fees::Error as XcmPaymentApiError,
@@ -111,8 +105,6 @@ use parachains_common::{
 
 #[cfg(feature = "runtime-benchmarks")]
 use alloc::boxed::Box;
-#[cfg(feature = "runtime-benchmarks")]
-use benchmark_helpers::DoNothingRouter;
 
 /// The address format for describing accounts.
 pub type Address = MultiAddress<AccountId, ()>;
@@ -542,174 +534,6 @@ impl pallet_utility::Config for Runtime {
 	type WeightInfo = weights::pallet_utility::WeightInfo<Runtime>;
 }
 
-// Ethereum Bridge
-parameter_types! {
-	pub storage EthereumGatewayAddress: H160 = H160(hex_literal::hex!("EDa338E4dC46038493b885327842fD3E301CaB39"));
-}
-
-parameter_types! {
-	pub const CreateAssetCall: [u8;2] = [53, 0];
-	pub const CreateAssetDeposit: u128 = (UNITS / 10) + EXISTENTIAL_DEPOSIT;
-	pub Parameters: PricingParameters<u128> = PricingParameters {
-		exchange_rate: FixedU128::from_rational(1, 400),
-		fee_per_gas: gwei(20),
-		rewards: Rewards { local: 1 * UNITS, remote: meth(1) },
-		multiplier: FixedU128::from_rational(1, 1),
-	};
-}
-
-#[cfg(feature = "runtime-benchmarks")]
-pub mod benchmark_helpers {
-	use crate::{EthereumBeaconClient, Runtime, RuntimeOrigin};
-	use codec::Encode;
-	use snowbridge_beacon_primitives::BeaconHeader;
-	use snowbridge_pallet_inbound_queue::BenchmarkHelper;
-	use sp_core::H256;
-	use xcm::latest::{Assets, Location, SendError, SendResult, SendXcm, Xcm, XcmHash};
-
-	impl<T: snowbridge_pallet_ethereum_client::Config> BenchmarkHelper<T> for Runtime {
-		fn initialize_storage(beacon_header: BeaconHeader, block_roots_root: H256) {
-			EthereumBeaconClient::store_finalized_header(beacon_header, block_roots_root).unwrap();
-		}
-	}
-
-	pub struct DoNothingRouter;
-	impl SendXcm for DoNothingRouter {
-		type Ticket = Xcm<()>;
-
-		fn validate(
-			_dest: &mut Option<Location>,
-			xcm: &mut Option<Xcm<()>>,
-		) -> SendResult<Self::Ticket> {
-			Ok((xcm.clone().unwrap(), Assets::new()))
-		}
-		fn deliver(xcm: Xcm<()>) -> Result<XcmHash, SendError> {
-			let hash = xcm.using_encoded(sp_io::hashing::blake2_256);
-			Ok(hash)
-		}
-	}
-
-	impl snowbridge_pallet_system::BenchmarkHelper<RuntimeOrigin> for () {
-		fn make_xcm_origin(location: Location) -> RuntimeOrigin {
-			RuntimeOrigin::from(pallet_xcm::Origin::Xcm(location))
-		}
-	}
-}
-
-impl snowbridge_pallet_inbound_queue::Config for Runtime {
-	type RuntimeEvent = RuntimeEvent;
-	type Verifier = snowbridge_pallet_ethereum_client::Pallet<Runtime>;
-	type Token = Balances;
-	#[cfg(not(feature = "runtime-benchmarks"))]
-	type XcmSender = XcmRouter;
-	#[cfg(feature = "runtime-benchmarks")]
-	type XcmSender = DoNothingRouter;
-	type ChannelLookup = EthereumSystem;
-	type GatewayAddress = EthereumGatewayAddress;
-	#[cfg(feature = "runtime-benchmarks")]
-	type Helper = Runtime;
-	type MessageConverter = MessageToXcm<
-		CreateAssetCall,
-		CreateAssetDeposit,
-		ConstU8<INBOUND_QUEUE_PALLET_INDEX>,
-		AccountId,
-		Balance,
-	>;
-	type WeightToFee = WeightToFee;
-	type LengthToFee = ConstantMultiplier<Balance, TransactionByteFee>;
-	type MaxMessageSize = ConstU32<2048>;
-	type WeightInfo = weights::snowbridge_pallet_inbound_queue::WeightInfo<Runtime>;
-	type PricingParameters = EthereumSystem;
-	type AssetTransactor = <xcm_config::XcmConfig as xcm_executor::Config>::AssetTransactor;
-}
-
-impl snowbridge_pallet_outbound_queue::Config for Runtime {
-	type RuntimeEvent = RuntimeEvent;
-	type Hashing = Keccak256;
-	type MessageQueue = MessageQueue;
-	type Decimals = ConstU8<12>;
-	type MaxMessagePayloadSize = ConstU32<2048>;
-	type MaxMessagesPerBlock = ConstU32<32>;
-	type GasMeter = snowbridge_core::outbound::ConstantGasMeter;
-	type Balance = Balance;
-	type WeightToFee = WeightToFee;
-	type WeightInfo = weights::snowbridge_pallet_outbound_queue::WeightInfo<Runtime>;
-	type PricingParameters = EthereumSystem;
-	type Channels = EthereumSystem;
-}
-
-#[cfg(any(feature = "std", feature = "fast-runtime", feature = "runtime-benchmarks", test))]
-parameter_types! {
-	pub const ChainForkVersions: ForkVersions = ForkVersions {
-		genesis: Fork {
-			version: [0, 0, 0, 0], // 0x00000000
-			epoch: 0,
-		},
-		altair: Fork {
-			version: [1, 0, 0, 0], // 0x01000000
-			epoch: 0,
-		},
-		bellatrix: Fork {
-			version: [2, 0, 0, 0], // 0x02000000
-			epoch: 0,
-		},
-		capella: Fork {
-			version: [3, 0, 0, 0], // 0x03000000
-			epoch: 0,
-		},
-		deneb: Fork {
-			version: [4, 0, 0, 0], // 0x04000000
-			epoch: 0,
-		}
-	};
-}
-
-#[cfg(not(any(feature = "std", feature = "fast-runtime", feature = "runtime-benchmarks", test)))]
-parameter_types! {
-	pub const ChainForkVersions: ForkVersions = ForkVersions {
-		genesis: Fork {
-			version: [144, 0, 0, 111], // 0x90000069
-			epoch: 0,
-		},
-		altair: Fork {
-			version: [144, 0, 0, 112], // 0x90000070
-			epoch: 50,
-		},
-		bellatrix: Fork {
-			version: [144, 0, 0, 113], // 0x90000071
-			epoch: 100,
-		},
-		capella: Fork {
-			version: [144, 0, 0, 114], // 0x90000072
-			epoch: 56832,
-		},
-		deneb: Fork {
-			version: [144, 0, 0, 115], // 0x90000073
-			epoch: 132608,
-		},
-	};
-}
-
-impl snowbridge_pallet_ethereum_client::Config for Runtime {
-	type RuntimeEvent = RuntimeEvent;
-	type ForkVersions = ChainForkVersions;
-	type WeightInfo = weights::snowbridge_pallet_ethereum_client::WeightInfo<Runtime>;
-}
-
-impl snowbridge_pallet_system::Config for Runtime {
-	type RuntimeEvent = RuntimeEvent;
-	type OutboundQueue = EthereumOutboundQueue;
-	type SiblingOrigin = EnsureXcm<AllowSiblingsOnly>;
-	type AgentIdOf = snowbridge_core::AgentIdOf;
-	type TreasuryAccount = TreasuryAccount;
-	type Token = Balances;
-	type WeightInfo = weights::snowbridge_pallet_system::WeightInfo<Runtime>;
-	#[cfg(feature = "runtime-benchmarks")]
-	type Helper = ();
-	type DefaultPricingParameters = Parameters;
-	type InboundDeliveryCost = EthereumInboundQueue;
-}
-
 // Create the runtime by composing the FRAME pallets that were previously configured.
 construct_runtime!(
 	pub enum Runtime
diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/tests/tests.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/tests/tests.rs
index 0daf90872189a2ae5dc8c8afa2e5e02d9314d6db..982c9fec6634496bd079b16177003ba9a5cf27cc 100644
--- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/tests/tests.rs
+++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/tests/tests.rs
@@ -18,11 +18,13 @@
 
 use bp_polkadot_core::Signature;
 use bridge_hub_rococo_runtime::{
-	bridge_common_config, bridge_to_bulletin_config, bridge_to_westend_config,
+	bridge_common_config, bridge_to_bulletin_config,
+	bridge_to_ethereum_config::EthereumGatewayAddress,
+	bridge_to_westend_config,
 	xcm_config::{LocationToAccountId, RelayNetwork, TokenLocation, XcmConfig},
-	AllPalletsWithoutSystem, BridgeRejectObsoleteHeadersAndMessages, EthereumGatewayAddress,
-	Executive, ExistentialDeposit, ParachainSystem, PolkadotXcm, Runtime, RuntimeCall,
-	RuntimeEvent, RuntimeOrigin, SessionKeys, SignedExtra, TransactionPayment, UncheckedExtrinsic,
+	AllPalletsWithoutSystem, BridgeRejectObsoleteHeadersAndMessages, Executive, ExistentialDeposit,
+	ParachainSystem, PolkadotXcm, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, SessionKeys,
+	SignedExtra, TransactionPayment, UncheckedExtrinsic,
 };
 use bridge_hub_test_utils::SlotDurations;
 use codec::{Decode, Encode};
diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs
index 7922d3ed02b1fe73293fd7bf4344fb5609c3aeda..47b6006ed6c1172d2b1bbbb67d73beeb97be65c0 100644
--- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs
+++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs
@@ -42,6 +42,8 @@ use sp_runtime::{
 	FixedU128,
 };
 
+pub const SLOTS_PER_EPOCH: u32 = snowbridge_pallet_ethereum_client::config::SLOTS_PER_EPOCH as u32;
+
 /// Exports message to the Ethereum Gateway contract.
 pub type SnowbridgeExporter = EthereumBlobExporter<
 	UniversalLocation,
@@ -163,6 +165,7 @@ parameter_types! {
 impl snowbridge_pallet_ethereum_client::Config for Runtime {
 	type RuntimeEvent = RuntimeEvent;
 	type ForkVersions = ChainForkVersions;
+	type FreeHeadersInterval = ConstU32<SLOTS_PER_EPOCH>;
 	type WeightInfo = crate::weights::snowbridge_pallet_ethereum_client::WeightInfo<Runtime>;
 }
 
diff --git a/prdoc/pr_5201.prdoc b/prdoc/pr_5201.prdoc
new file mode 100644
index 0000000000000000000000000000000000000000..a0c1bbfd2e413784bdddab0fcd27e1ad586ff8d9
--- /dev/null
+++ b/prdoc/pr_5201.prdoc
@@ -0,0 +1,23 @@
+# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0
+# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json
+
+title: Snowbridge free consensus updates
+
+doc:
+  - audience: Runtime Dev
+    description: |
+      Allow free consensus updates to the Snowbridge Ethereum client if the headers are more than a certain
+      number of headers apart. Relayers providing valid consensus updates are refunded for updates. Bridge
+      users are not affected.
+
+crates:
+  - name: snowbridge-pallet-ethereum-client
+    bump: patch
+  - name: snowbridge-pallet-inbound-queue
+    bump: patch
+  - name: snowbridge-runtime-test-common
+    bump: patch
+  - name: bridge-hub-rococo-runtime
+    bump: major
+  - name: bridge-hub-westend-runtime
+    bump: major