Repository: sveltejs/svelte (2025-07-22T22:33:19Z)

Issue:
Async,  Streaming with Promises & Infinite Loop
### Describe the bug

The console gets spammed by `Tick updated at:` messages. More than it should.

Note that I don't see this issue if I rename `+page.server.ts` to `+page.ts`.

Unclear to me if I'm doing something I shouldn't or if this is a real issue. Please be gentle.

### Reproduction

+page.server.ts
```typescript
export const load = async ({fetch}) => {
  const res = await fetch('https://pokeapi.co/api/v2/pokemon/pikachu');
  return {data: res.json()}
}
```

+page.svelte
```svelte
<script lang="ts">

import type {PageProps} from "./$types";
import {readable} from "svelte/store";
import {invalidateAll} from "$app/navigation";

const {data}: PageProps = $props();

const tick = readable(Date.now(), (set) => {
  const id = setInterval(() => set(Date.now()), 1000);
  return () => clearInterval(id);
});

$effect(() => {
  if ($tick) {
    console.log("Tick updated at:", $tick);
    invalidateAll();
  }
});

</script>

<main>
  <svelte:boundary>
    <p>{JSON.stringify((await data.data), null, 2)}</p>

    {#snippet pending()}
      <p>loading...</p>
    {/snippet}
  </svelte:boundary>
</main>
```

### Logs

```shell
Tick updated at: 1752693977687 +page.svelte:16:13
Tick updated at: 1752693977687 +page.svelte:16:13
Tick updated at: 1752693977687 +page.svelte:16:13
Tick updated at: 1752693978697 +page.svelte:16:13
Tick updated at: 1752693978697 +page.svelte:16:13
Tick updated at: 1752693978697 +page.svelte:16:13
Tick updated at: 1752693978697 +page.svelte:16:13
Tick updated at: 1752693978697 +page.svelte:16:13
Tick updated at: 1752693978697 +page.svelte:16:13
Tick updated at: 1752693978697 +page.svelte:16:13
Tick updated at: 1752693978697 +page.svelte:16:13
Tick updated at: 1752693978697 +page.svelte:16:13
Tick updated at: 1752693978697 +page.svelte:16:13
Tick updated at: 1752693978697 +page.svelte:16:13
Tick updated at: 1752693978697 +page.svelte:16:13
```

### System Info

```shell
System:
    OS: Linux 6.15 Manjaro Linux
    CPU: (8) x64 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz
    Memory: 10.87 GB / 39.02 GB
    Container: Yes
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 22.17.0 - ~/.nvm/versions/node/v22.17.0/bin/node
    Yarn: 1.22.21 - ~/.nvm/versions/node/v20.9.0/bin/yarn
    npm: 10.9.2 - ~/.nvm/versions/node/v22.17.0/bin/npm
    pnpm: 9.10.0 - ~/.nvm/versions/node/v20.9.0/bin/pnpm
    bun: 1.2.18 - /usr/bin/bun
  Browsers:
    Chromium: 138.0.7204.92
  npmPackages:
    svelte: 5.36.4 => 5.36.4
```

### Severity

annoyance
input bind doesn't work as expected since 5.36.0
### Describe the bug

There is a number input where the minimum and maximum values limit the number. When the user enters a number less or greater than the limits, the value will be set to the minimum or maximum value.

It works fine in versions 5.35.6 and earlier.

### Reproduction

It works fine here:
https://svelte.dev/playground/04a9aff17b1b422c86782ac4b30acf0d?version=5.35.6

But it failing here:
https://svelte.dev/playground/04a9aff17b1b422c86782ac4b30acf0d?version=5.36.5

### Logs

```shell

```

### System Info

```shell
System:
    OS: macOS 15.5
    CPU: (14) arm64 Apple M3 Max
    Memory: 143.45 MB / 36.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 22.14.0 - ~/.nvm/versions/node/v22.14.0/bin/node
    npm: 10.9.2 - ~/.nvm/versions/node/v22.14.0/bin/npm
    pnpm: 10.6.4 - ~/.nvm/versions/node/v22.14.0/bin/pnpm
  Browsers:
    Chrome: 138.0.7204.157
    Edge: 138.0.3351.83
    Safari: 18.5
  npmPackages:
    svelte: 5.35.6 => 5.35.6
```

### Severity

blocking an upgrade
Probably false positive effect_update_depth_exceeded error when reading derived firstly at effect
### Describe the bug

In reproduce I am rendering 1000+ elements in each block. Each element has state that never changes and derived dependent on this state. Dervied firstly read inside $effect. Then there is more than 1000 elemtns the effect_update_depth_exceeded error is fired. But in fact there should not be any loops in effects

### Reproduction

The reproduction is presented here https://svelte.dev/playground/d9c15933109c478181c3a00854397dec?version=5.36.6

Firstly test it with 1000 elements by clicking the show button - there will be no error
Increase number of elemetns to 1001 and do the same again - the error will appear
Uncomment reading canPlay derived in Component.svelte to calculate it before effect - there will be no error


### Logs

```shell

```

### System Info

```shell
System:
    OS: macOS 15.5
    CPU: (12) arm64 Apple M4 Pro
    Memory: 65.36 MB / 24.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 22.17.0 - ~/.nvm/versions/node/v22.17.0/bin/node
    Yarn: 1.22.22 - ~/.nvm/versions/node/v22.17.0/bin/yarn
    npm: 10.9.2 - ~/.nvm/versions/node/v22.17.0/bin/npm
    pnpm: 10.12.3 - /opt/homebrew/bin/pnpm
  Browsers:
    Chrome: 138.0.7204.158
    Safari: 18.5
  npmPackages:
    svelte: ^5.36.6 => 5.36.6
```

### Severity

blocking all usage of svelte


Discussion:
Haven't dived into this with any depth at all so this may be completely wrong, but what happens if you replace the `invalidateAll()` call with `untrack(() => invalidateAll())` in that `$effect`?
> Haven't dived into this with any depth at all so this may be completely wrong, but what happens if you replace the `invalidateAll()` call with `untrack(() => invalidateAll())` in that `$effect`?

I'm getting the same issue.
Minimum reproducible:

```svelte
<script lang="ts">
let data = $state(Promise.resolve(Math.random()));

let tick = $state(Date.now());
$effect(() => {
	const x = setInterval(() => {
		tick = Date.now();
	}, 1000);
	return () => clearInterval(x);
})

$effect(() => {
	if (tick) {
		console.log("Tick updated at:", tick);
		data = Promise.resolve(Math.random())
  }
});

</script>

<main>
  <svelte:boundary>
    <p>{JSON.stringify((await data), null, 2)}</p>

    {#snippet pending()}
      <p>loading...</p>
    {/snippet}
  </svelte:boundary>
</main>
```

This happens because `batch.decrement()` executes all effects, whether or not they are actually dirty.

Can I work on this? 
sure, go ahead!
@kishansinghifs1 
I found that this line of code occurs this issue.

https://github.com/yuki0418/svelte/blob/27c90dfa8322c9f225e22a730a776c4f4cbd2c5e/packages/svelte/src/internal/client/dom/elements/bindings/input.js#L79-L86
Maybe `#neutered` inside `Batch` can be used for detect async effect?

Hello! I have a very similar issue after updating svelte. I do not have reproduction code handy, but I do know that I have very few effects and none of them are recursive. The page this reproduces on in my app also loads a thousand plus components in a loop. Pages with fewer items in the list don't seem to have this issue. In other words if I load the same code with fewer child-components being generated it works fine. **This seems to be a stack size problem more than a recursion problem.**

EDIT: Tried a bunch of svelte versions. The latest version I can use without errors is 5.35.3. The 5.35.4 release introduces this problem which seems to include only this contribution: https://github.com/sveltejs/svelte/pull/16280

EDIT2: Just to make sure I don't have any control over this I removed all the effects from my app, and this still happens. It's possible there are effects in some third party module I'm loading, but I really doubt that as most of them are still svelte 4 format.
Some additional info

I have tested my reproduction on both ^5.36.6 and ^5.35.5. In our project we still use ^5.35.5. I have checked the source code and it seems to be that effect_update_depth_exceeded error fires if 1000+ effects flushed in one function call. https://github.com/sveltejs/svelte/blob/05f6436445d54a689ca12cced4bd359d15fc2e4c/packages/svelte/src/internal/client/reactivity/batch.js#L335

It checks if the number of effects is more than a 1000. And if so, it throws and error. Of course I might be wrong due to the lack of code knowledge

It seems like the error is just as symptom of something weird going on.

In our case the browser hangs in this situation in a similar way how it would hang if there is an infinite `$effect` loop. However from the code itself it does not look like there is a loop. Also downgrading to 5.35.3 or making a workaround written in the OP does seem to remove the issue.

The bigger problem is that recently our users report tab randomly hanging. For now we were not able to identify the root cause entirely, but discovered the case written here. Now I am wondering if this could be the root cause, since our version update kind of matches the timeline.
Having the same issue on our codebase, however, 5.35.4 works fine for us but after 5.35.5 the same issue appears.
Yeah the problem is introduced in #16280 ...the problem is that the derived is updated within the effect, which means that the effect is rescheduled...it's not really a problem because it's not dirty but the counter doesn't reset so if you have more than 1000 effects it throws the error.

Not sure how to fix it (resetting the counter will make the safeguard fail for the actual use cases
@paoloricciuti is it possible that this rescheduling can cause some loops? Because the tab freezing bugs me more about this the the error itself
> [@paoloricciuti](https://github.com/paoloricciuti) is it possible that this rescheduling can cause some loops? Because the tab freezing bugs me more about this the the error itself

It shouldn't...do you have a repro?
So far we have a stable repro in our project itself, we are actively working on extracting this into a separate repro
I've adjusted [REPL](https://svelte.dev/playground/d9c15933109c478181c3a00854397dec?version=5.35.5#H4sIAAAAAAAAE51TwW7bMAz9FU4tUAcIouzq2gaGXXYcMOxUFYFq0bVQmTIkOklh-N8H2UmWbt2A7WBDfnx8Jh-pUZDuUOTiCzrn4eCDM5ChsYxmJdaisQ6jyB9Gwa994iVArM9Zn_p-E_foOGFPOuJ7eO2JkTiKXBQLmPueracIYSCMICtFRayD7blSpNh2vQ8Mn33Xe0JiaILv4G4jL8hJ_C6x01N7igw6BCjh4fE-QVKmbl4iHCy3KbRxSM_cQlHCx-12e-Lgsca5GFDiO9V6eG4Zvs3qgCH4kAM2Dda8G3qjGXcGe253KQ0NGiV-068u8lfg8s_7M9pY57LtKn2TYocMsfUHKOE2smbMGu0irhQV8mJL8TQwewJPtbP1Szlmq7JKWeWH9J5m59IpZS3cStF4Y5tZe0rh8QZ1PRcLOoJl7GZYcfHTazkLjTIxJ0WjtM0k1oLxyCLnMOC0_sMy_DqdtyvxTvSfF2MZc63pq9OvyS2Dwe7RnPw62SklDFT7rkvtEB4ZnCUE9qD33pr_HjTukf4ybSnPlZ238nYRzLIVlBWMi9VXnClVfDViRYWx-6pNV7GQ6fjW-Me1YG3dwZIR-dzy9ANDGGJTwAMAAA==) enough so you can see a degradation. With simple `<div>hello</div>` it's not really problematic in absolute numbers, but in our cases it make everything freeze. In our case for example the relative drop in performance 4x. And I am wondering if this problem can result in some infinite loops.

Here is how much click takes on 5.35.5
<img width="1057" height="212" alt="Image" src="https://github.com/user-attachments/assets/79b161c3-b445-46c4-937c-55d44a19ab18" />

And here how it is on 5.35.3
<img width="532" height="448" alt="Image" src="https://github.com/user-attachments/assets/b109e9d3-fb48-4ed9-8e83-2adc01edde2a" />
Tested the just released [5.36.14](https://github.com/sveltejs/svelte/releases/tag/svelte%405.36.14), but the situation is rather getting worse in our case. The same code now hangs the browser, causing an infinite loop of `effect_update_depth_exceeded` rather than just showing the uncaught error once in the console.
Then it's not the same issue — you'll need to open a new ticket with a repro
We really need some kind of reproducible then - it doesn't even have to be minimal in this case
I’ve already solved the problem [on our end](https://github.com/videomark/videomark/pull/362/files), but will try to create a simple reproduction and file a new ticket.
> I’ve already solved the problem [on our end](https://github.com/videomark/videomark/pull/362/files), but will try to create a simple reproduction and file a new ticket.

I'm very surprised this worked before: you are reading and writing to the same stateful variable...that's an infinite loop since day 1 of svelte 5
> I'm very surprised this worked before: you are reading and writing to the same stateful variable...that's an infinite loop since day 1 of svelte 5

I was also surprised, actually. When I migrated [Sveltia CMS](https://github.com/sveltia/sveltia-cms) to Svelte 5, I encountered the `effect_update_depth_exceeded` error a dozen times with similar code, so I know how it could happen. For some reason, however, that particular code in another project was overlooked, yet it worked up until Svelte 5.35.3.
I have updated to 5.36.14
It solved the issue, but yields `effect_update_depth_exceeded` error in production only. It doesn't hang the browser nor cause any rendering issues.



Patch:
diff --git a/.changeset/grumpy-boats-beg.md b/.changeset/grumpy-boats-beg.md
new file mode 100644
index 000000000000..f677743defa5
--- /dev/null
+++ b/.changeset/grumpy-boats-beg.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: keep input in sync when binding updated via effect
diff --git a/.changeset/shiny-walls-fix.md b/.changeset/shiny-walls-fix.md
new file mode 100644
index 000000000000..91ed548728a3
--- /dev/null
+++ b/.changeset/shiny-walls-fix.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: prevent infinite async loop
diff --git a/.changeset/thick-mice-kick.md b/.changeset/thick-mice-kick.md
new file mode 100644
index 000000000000..eec55b77eee4
--- /dev/null
+++ b/.changeset/thick-mice-kick.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: exclude derived writes from effect abort and rescheduling
diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js
index c45221189404..ce413fa1e186 100644
--- a/packages/svelte/src/internal/client/reactivity/batch.js
+++ b/packages/svelte/src/internal/client/reactivity/batch.js
@@ -21,14 +21,13 @@ import {
 	is_updating_effect,
 	set_is_updating_effect,
 	set_signal_status,
-	update_effect,
-	write_version
+	update_effect
 } from '../runtime.js';
 import * as e from '../errors.js';
 import { flush_tasks } from '../dom/task.js';
 import { DEV } from 'esm-env';
 import { invoke_error_boundary } from '../error-handling.js';
-import { old_values } from './sources.js';
+import { mark_reactions, old_values } from './sources.js';
 import { unlink_effect } from './effects.js';
 import { unset_context } from './async.js';
 
@@ -70,13 +69,15 @@ let last_scheduled_effect = null;
 
 let is_flushing = false;
 
+let is_flushing_sync = false;
+
 export class Batch {
 	/**
 	 * The current values of any sources that are updated in this batch
 	 * They keys of this map are identical to `this.#previous`
 	 * @type {Map<Source, any>}
 	 */
-	#current = new Map();
+	current = new Map();
 
 	/**
 	 * The values of any sources that are updated in this batch _before_ those updates took place.
@@ -156,7 +157,7 @@ export class Batch {
 	 *
 	 * @param {Effect[]} root_effects
 	 */
-	#process(root_effects) {
+	process(root_effects) {
 		queued_root_effects = [];
 
 		/** @type {Map<Source, { v: unknown, wv: number }> | null} */
@@ -169,7 +170,7 @@ export class Batch {
 			current_values = new Map();
 			batch_deriveds = new Map();
 
-			for (const [source, current] of this.#current) {
+			for (const [source, current] of this.current) {
 				current_values.set(source, { v: source.v, wv: source.wv });
 				source.v = current;
 			}
@@ -202,9 +203,22 @@ export class Batch {
 			this.#effects = [];
 			this.#block_effects = [];
 
+			// If sources are written to, then work needs to happen in a separate batch, else prior sources would be mixed with
+			// newly updated sources, which could lead to infinite loops when effects run over and over again.
+			current_batch = null;
+
 			flush_queued_effects(render_effects);
 			flush_queued_effects(effects);
 
+			// Reinstate the current batch if there was no new one created, as `process()` runs in a loop in `flush_effects()`.
+			// That method expects `current_batch` to be set, and could run the loop again if effects result in new effects
+			// being scheduled but without writes happening in which case no new batch is created.
+			if (current_batch === null) {
+				current_batch = this;
+			} else {
+				batches.delete(this);
+			}
+
 			this.#deferred?.resolve();
 		} else {
 			// otherwise mark effects clean so they get scheduled on the next run
@@ -300,7 +314,7 @@ export class Batch {
 			this.#previous.set(source, value);
 		}
 
-		this.#current.set(source, source.v);
+		this.current.set(source, source.v);
 	}
 
 	activate() {
@@ -327,13 +341,13 @@ export class Batch {
 
 	flush() {
 		if (queued_root_effects.length > 0) {
-			this.flush_effects();
+			flush_effects();
 		} else {
 			this.#commit();
 		}
 
 		if (current_batch !== this) {
-			// this can happen if a `flushSync` occurred during `this.flush_effects()`,
+			// this can happen if a `flushSync` occurred during `flush_effects()`,
 			// which is permitted in legacy mode despite being a terrible idea
 			return;
 		}
@@ -345,52 +359,6 @@ export class Batch {
 		this.deactivate();
 	}
 
-	flush_effects() {
-		var was_updating_effect = is_updating_effect;
-		is_flushing = true;
-
-		try {
-			var flush_count = 0;
-			set_is_updating_effect(true);
-
-			while (queued_root_effects.length > 0) {
-				if (flush_count++ > 1000) {
-					if (DEV) {
-						var updates = new Map();
-
-						for (const source of this.#current.keys()) {
-							for (const [stack, update] of source.updated ?? []) {
-								var entry = updates.get(stack);
-
-								if (!entry) {
-									entry = { error: update.error, count: 0 };
-									updates.set(stack, entry);
-								}
-
-								entry.count += update.count;
-							}
-						}
-
-						for (const update of updates.values()) {
-							// eslint-disable-next-line no-console
-							console.error(update.error);
-						}
-					}
-
-					infinite_loop_guard();
-				}
-
-				this.#process(queued_root_effects);
-				old_values.clear();
-			}
-		} finally {
-			is_flushing = false;
-			set_is_updating_effect(was_updating_effect);
-
-			last_scheduled_effect = null;
-		}
-	}
-
 	/**
 	 * Append and remove branches to/from the DOM
 	 */
@@ -412,19 +380,8 @@ export class Batch {
 		this.#pending -= 1;
 
 		if (this.#pending === 0) {
-			for (const e of this.#render_effects) {
-				set_signal_status(e, DIRTY);
-				schedule_effect(e);
-			}
-
-			for (const e of this.#effects) {
-				set_signal_status(e, DIRTY);
-				schedule_effect(e);
-			}
-
-			for (const e of this.#block_effects) {
-				set_signal_status(e, DIRTY);
-				schedule_effect(e);
+			for (const source of this.current.keys()) {
+				mark_reactions(source, DIRTY, false);
 			}
 
 			this.#render_effects = [];
@@ -445,12 +402,12 @@ export class Batch {
 		return (this.#deferred ??= deferred()).promise;
 	}
 
-	static ensure(autoflush = true) {
+	static ensure() {
 		if (current_batch === null) {
 			const batch = (current_batch = new Batch());
 			batches.add(current_batch);
 
-			if (autoflush) {
+			if (!is_flushing_sync) {
 				Batch.enqueue(() => {
 					if (current_batch !== batch) {
 						// a flushSync happened in the meantime
@@ -487,32 +444,85 @@ export function flushSync(fn) {
 		e.flush_sync_in_effect();
 	}
 
-	var result;
+	var was_flushing_sync = is_flushing_sync;
+	is_flushing_sync = true;
 
-	const batch = Batch.ensure(false);
+	try {
+		var result;
 
-	if (fn) {
-		batch.flush_effects();
+		if (fn) {
+			flush_effects();
+			result = fn();
+		}
 
-		result = fn();
-	}
+		while (true) {
+			flush_tasks();
 
-	while (true) {
-		flush_tasks();
+			if (queued_root_effects.length === 0) {
+				current_batch?.flush();
 
-		if (queued_root_effects.length === 0) {
-			if (batch === current_batch) {
-				batch.flush();
+				// we need to check again, in case we just updated an `$effect.pending()`
+				if (queued_root_effects.length === 0) {
+					// this would be reset in `flush_effects()` but since we are early returning here,
+					// we need to reset it here as well in case the first time there's 0 queued root effects
+					last_scheduled_effect = null;
+
+					return /** @type {T} */ (result);
+				}
 			}
 
-			// this would be reset in `batch.flush_effects()` but since we are early returning here,
-			// we need to reset it here as well in case the first time there's 0 queued root effects
-			last_scheduled_effect = null;
+			flush_effects();
+		}
+	} finally {
+		is_flushing_sync = was_flushing_sync;
+	}
+}
+
+function flush_effects() {
+	var was_updating_effect = is_updating_effect;
+	is_flushing = true;
+
+	try {
+		var flush_count = 0;
+		set_is_updating_effect(true);
+
+		while (queued_root_effects.length > 0) {
+			var batch = Batch.ensure();
+
+			if (flush_count++ > 1000) {
+				if (DEV) {
+					var updates = new Map();
+
+					for (const source of batch.current.keys()) {
+						for (const [stack, update] of source.updated ?? []) {
+							var entry = updates.get(stack);
+
+							if (!entry) {
+								entry = { error: update.error, count: 0 };
+								updates.set(stack, entry);
+							}
+
+							entry.count += update.count;
+						}
+					}
+
+					for (const update of updates.values()) {
+						// eslint-disable-next-line no-console
+						console.error(update.error);
+					}
+				}
+
+				infinite_loop_guard();
+			}
 
-			return /** @type {T} */ (result);
+			batch.process(queued_root_effects);
+			old_values.clear();
 		}
+	} finally {
+		is_flushing = false;
+		set_is_updating_effect(was_updating_effect);
 
-		batch.flush_effects();
+		last_scheduled_effect = null;
 	}
 }
 
@@ -545,7 +555,7 @@ function flush_queued_effects(effects) {
 		var effect = effects[i++];
 
 		if ((effect.f & (DESTROYED | INERT)) === 0 && is_dirty(effect)) {
-			var wv = write_version;
+			var n = current_batch ? current_batch.current.size : 0;
 
 			update_effect(effect);
 
@@ -568,7 +578,11 @@ function flush_queued_effects(effects) {
 
 			// if state is written in a user effect, abort and re-schedule, lest we run
 			// effects that should be removed as a result of the state change
-			if (write_version > wv && (effect.f & USER_EFFECT) !== 0) {
+			if (
+				current_batch !== null &&
+				current_batch.current.size > n &&
+				(effect.f & USER_EFFECT) !== 0
+			) {
 				break;
 			}
 		}
diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js
index f6b14f3360de..3b28c8fdceeb 100644
--- a/packages/svelte/src/internal/client/reactivity/sources.js
+++ b/packages/svelte/src/internal/client/reactivity/sources.js
@@ -179,7 +179,7 @@ export function internal_set(source, value) {
 
 		source.v = value;
 
-		const batch = Batch.ensure();
+		var batch = Batch.ensure();
 		batch.capture(source, old_value);
 
 		if (DEV) {
@@ -301,9 +301,10 @@ export function increment(source) {
 /**
  * @param {Value} signal
  * @param {number} status should be DIRTY or MAYBE_DIRTY
+ * @param {boolean} schedule_async
  * @returns {void}
  */
-function mark_reactions(signal, status) {
+export function mark_reactions(signal, status, schedule_async = true) {
 	var reactions = signal.reactions;
 	if (reactions === null) return;
 
@@ -323,14 +324,16 @@ function mark_reactions(signal, status) {
 			continue;
 		}
 
+		var should_schedule = (flags & DIRTY) === 0 && (schedule_async || (flags & ASYNC) === 0);
+
 		// don't set a DIRTY reaction to MAYBE_DIRTY
-		if ((flags & DIRTY) === 0) {
+		if (should_schedule) {
 			set_signal_status(reaction, status);
 		}
 
 		if ((flags & DERIVED) !== 0) {
 			mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY);
-		} else if ((flags & DIRTY) === 0) {
+		} else if (should_schedule) {
 			schedule_effect(/** @type {Effect} */ (reaction));
 		}
 	}


Test patch:
diff --git a/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/Component.svelte b/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/Component.svelte
new file mode 100644
index 000000000000..7a54323cb97f
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/Component.svelte
@@ -0,0 +1,7 @@
+<script>
+	const der = $derived(false);
+
+	$effect(() => {
+		der
+	});
+</script>
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/_config.js b/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/_config.js
new file mode 100644
index 000000000000..2e4a27cf0912
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/_config.js
@@ -0,0 +1,5 @@
+import { test } from '../../test';
+
+export default test({
+	async test() {}
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/main.svelte b/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/main.svelte
new file mode 100644
index 000000000000..bd326edfb92a
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/main.svelte
@@ -0,0 +1,8 @@
+<script>
+	import Component from './Component.svelte';
+	const arr = Array.from({length: 10001});
+</script>
+
+{#each arr}
+	<Component />
+{/each}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused/_config.js b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused/_config.js
new file mode 100644
index 000000000000..782ae945f9c3
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused/_config.js
@@ -0,0 +1,26 @@
+import { tick } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+	async test({ assert, target }) {
+		await tick();
+
+		const [input] = target.querySelectorAll('input');
+
+		input.focus();
+		input.value = '3';
+		input.dispatchEvent(new InputEvent('input', { bubbles: true }));
+		await tick();
+
+		assert.equal(input.value, '3');
+		assert.htmlEqual(target.innerHTML, `<p>3</p> <input type="number" />`);
+
+		input.focus();
+		input.value = '1';
+		input.dispatchEvent(new InputEvent('input', { bubbles: true }));
+		await tick();
+
+		assert.equal(input.value, '2');
+		assert.htmlEqual(target.innerHTML, `<p>2</p> <input type="number" />`);
+	}
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused/main.svelte
new file mode 100644
index 000000000000..763ce6ebf073
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused/main.svelte
@@ -0,0 +1,27 @@
+<script>
+	let value = $state(0)
+	const min = 2
+	const max = 5
+
+	$effect(() => {
+		setValue()
+	})
+
+	function setValue() {
+		if (value < min) {
+			value = min
+		}
+		if (value > max) {
+			value = max
+		}
+	}
+</script>
+
+<svelte:boundary>
+	<p>{await value}</p>
+	<input type="number" bind:value />
+
+	{#snippet pending()}
+		<p>loading...</p>
+	{/snippet}
+</svelte:boundary>
diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-triggers-await/_config.js b/packages/svelte/tests/runtime-runes/samples/async-effect-triggers-await/_config.js
new file mode 100644
index 000000000000..c551cc6b8c39
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-effect-triggers-await/_config.js
@@ -0,0 +1,32 @@
+import { tick } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+	async test({ assert, target }) {
+		await tick();
+
+		const [increment] = target.querySelectorAll('button');
+
+		increment.click();
+		await tick();
+		assert.htmlEqual(
+			target.innerHTML,
+			`
+				<button>increment</button>
+				<p>1</p>
+				<p>1</p>
+			`
+		);
+
+		increment.click();
+		await tick();
+		assert.htmlEqual(
+			target.innerHTML,
+			`
+				<button>increment</button>
+				<p>2</p>
+				<p>2</p>
+			`
+		);
+	}
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-triggers-await/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-effect-triggers-await/main.svelte
new file mode 100644
index 000000000000..153fe03f0d89
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-effect-triggers-await/main.svelte
@@ -0,0 +1,24 @@
+<script lang="ts">
+	let data = $state(Promise.resolve(0));
+
+	let count = $state(0);
+	let unrelated = $state(0);
+
+	$effect(() => {
+		data = Promise.resolve(count)
+		unrelated = count;
+	});
+</script>
+
+<svelte:boundary>
+	<button onclick={() => count += 1}>increment</button>
+	<p>{JSON.stringify((await data), null, 2)}</p>
+	{#if true}
+		<!-- inside if block to force it into a different render effect -->
+		<p>{unrelated}</p>
+	{/if}
+
+	{#snippet pending()}
+		<p>loading...</p>
+	{/snippet}
+</svelte:boundary>
diff --git a/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-2/_config.js b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-2/_config.js
new file mode 100644
index 000000000000..7a56c79d7176
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-2/_config.js
@@ -0,0 +1,24 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+	test({ assert, target }) {
+		const [input] = target.querySelectorAll('input');
+
+		input.focus();
+		input.value = '3';
+		input.dispatchEvent(new InputEvent('input', { bubbles: true }));
+		flushSync();
+
+		assert.equal(input.value, '3');
+		assert.htmlEqual(target.innerHTML, `<p>3</p> <input type="number" />`);
+
+		input.focus();
+		input.value = '1';
+		input.dispatchEvent(new InputEvent('input', { bubbles: true }));
+		flushSync();
+
+		assert.equal(input.value, '2');
+		assert.htmlEqual(target.innerHTML, `<p>2</p> <input type="number" />`);
+	}
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-2/main.svelte
new file mode 100644
index 000000000000..b0597c223b99
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-2/main.svelte
@@ -0,0 +1,21 @@
+<script>
+	let value = $state(0)
+	const min = 2
+	const max = 5
+
+	$effect(() => {
+		setValue()
+	})
+
+	function setValue() {
+		if (value < min) {
+			value = min
+		}
+		if (value > max) {
+			value = max
+		}
+	}
+</script>
+
+<p>{value}</p>
+<input type="number" bind:value />