diff --git a/.changeset/soft-clocks-remember.md b/.changeset/soft-clocks-remember.md
new file mode 100644
index 0000000000..69e8aca06e
--- /dev/null
+++ b/.changeset/soft-clocks-remember.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: improve consistency issues around binding invalidation
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js
index 6579d17c01..6e3e570454 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js
@@ -245,7 +245,7 @@ function setup_select_synchronization(value_binding, context) {
 	context.state.init.push(
 		b.stmt(
 			b.call(
-				'$.pre_effect',
+				'$.invalidate_effect',
 				b.thunk(
 					b.block([
 						b.stmt(
diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js
index 5d92c436e8..f075d2d5a3 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -1273,6 +1273,17 @@ export function pre_effect(init) {
 	);
 }
 
+/**
+ * This effect is used to ensure binding are kept in sync. We use a pre effect to ensure we run before the
+ * bindings which are in later effects. However, we don't use a pre_effect directly as we don't want to flush anything.
+ *
+ * @param {() => void | (() => void)} init
+ * @returns {import('./types.js').EffectSignal}
+ */
+export function invalidate_effect(init) {
+	return internal_create_effect(PRE_EFFECT, init, true, current_block, true);
+}
+
 /**
  * @param {() => void | (() => void)} init
  * @returns {import('./types.js').EffectSignal}
diff --git a/packages/svelte/src/internal/index.js b/packages/svelte/src/internal/index.js
index 4d3205b824..a94a0de180 100644
--- a/packages/svelte/src/internal/index.js
+++ b/packages/svelte/src/internal/index.js
@@ -12,6 +12,7 @@ export {
 	user_effect,
 	render_effect,
 	pre_effect,
+	invalidate_effect,
 	flushSync,
 	bubble_event,
 	safe_equal,
diff --git a/packages/svelte/tests/runtime-runes/samples/invalidate-effect/_config.js b/packages/svelte/tests/runtime-runes/samples/invalidate-effect/_config.js
new file mode 100644
index 0000000000..b4ba660ad4
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/invalidate-effect/_config.js
@@ -0,0 +1,16 @@
+import { test } from '../../test';
+
+export default test({
+	async test({ assert, target }) {
+		assert.htmlEqual(target.innerHTML, 'a\n<select></select><button>change</button');
+
+		const [b1] = target.querySelectorAll('button');
+		b1.click();
+		await Promise.resolve();
+
+		assert.htmlEqual(
+			target.innerHTML,
+			'a\n<select></select>b\n<select></select><button>change</button'
+		);
+	}
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/invalidate-effect/main.svelte b/packages/svelte/tests/runtime-runes/samples/invalidate-effect/main.svelte
new file mode 100644
index 0000000000..895a940451
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/invalidate-effect/main.svelte
@@ -0,0 +1,13 @@
+<script>
+	let entries = $state([{selected: 'a'}])
+</script>
+
+{#each entries as entry}
+	{entry.selected} <select bind:value={entry.selected}></select>
+{/each}
+
+<button
+	on:click={
+		() => entries = [{selected: 'a'}, {selected: 'b'}]
+	}
+	>change</button>