diff --git a/packages/nuxt/src/app/composables/once.ts b/packages/nuxt/src/app/composables/once.ts
index 177ab4c9d5..ae8746fff0 100644
--- a/packages/nuxt/src/app/composables/once.ts
+++ b/packages/nuxt/src/app/composables/once.ts
@@ -1,18 +1,23 @@
 import { useNuxtApp } from '../nuxt'
 
+type CallOnceOptions = {
+  mode?: 'navigation' | 'render'
+}
+
 /**
  * An SSR-friendly utility to call a method once
  * @param key a unique key ensuring the function can be properly de-duplicated across requests
  * @param fn a function to call
+ * @param options Setup the mode, e.g. to re-execute on navigation
  * @see https://nuxt.com/docs/api/utils/call-once
  * @since 3.9.0
  */
-export function callOnce (key?: string, fn?: (() => any | Promise<any>)): Promise<void>
-export function callOnce (fn?: (() => any | Promise<any>)): Promise<void>
+export function callOnce (key?: string, fn?: (() => any | Promise<any>), options?: CallOnceOptions): Promise<void>
+export function callOnce (fn?: (() => any | Promise<any>), options?: CallOnceOptions): Promise<void>
 export async function callOnce (...args: any): Promise<void> {
   const autoKey = typeof args[args.length - 1] === 'string' ? args.pop() : undefined
   if (typeof args[0] !== 'string') { args.unshift(autoKey) }
-  const [_key, fn] = args as [string, (() => any | Promise<any>)]
+  const [_key, fn, options] = args as [string, (() => any | Promise<any>), CallOnceOptions | undefined]
   if (!_key || typeof _key !== 'string') {
     throw new TypeError('[nuxt] [callOnce] key must be a string: ' + _key)
   }
@@ -20,10 +25,18 @@ export async function callOnce (...args: any): Promise<void> {
     throw new Error('[nuxt] [callOnce] fn must be a function: ' + fn)
   }
   const nuxtApp = useNuxtApp()
+
+  if (options?.mode === 'navigation') {
+    nuxtApp.hooks.hookOnce('page:start', () => {
+      nuxtApp.payload.once.delete(_key)
+    })
+  }
+
   // If key already ran
   if (nuxtApp.payload.once.has(_key)) {
     return
   }
+
   nuxtApp._once = nuxtApp._once || {}
   nuxtApp._once[_key] = nuxtApp._once[_key] || fn() || true
   await nuxtApp._once[_key]
diff --git a/packages/schema/src/config/build.ts b/packages/schema/src/config/build.ts
index d884c89a0e..debe037a03 100644
--- a/packages/schema/src/config/build.ts
+++ b/packages/schema/src/config/build.ts
@@ -147,7 +147,7 @@ export default defineUntypedSchema({
     keyedComposables: {
       $resolve: (val: Array<{ name: string, argumentLength: string }> | undefined) => [
         { name: 'useId', argumentLength: 1 },
-        { name: 'callOnce', argumentLength: 2 },
+        { name: 'callOnce', argumentLength: 3 },
         { name: 'defineNuxtComponent', argumentLength: 2 },
         { name: 'useState', argumentLength: 2 },
         { name: 'useFetch', argumentLength: 3 },
diff --git a/test/basic.test.ts b/test/basic.test.ts
index 2c00fb9ce2..a3fc3e4a8f 100644
--- a/test/basic.test.ts
+++ b/test/basic.test.ts
@@ -1226,6 +1226,15 @@ describe('composables', () => {
     const { page } = await renderPage('/once')
     expect(await page.getByText('once:').textContent()).toContain('once: 2')
   })
+  it('`callOnce` should run code once with navigation mode during initial render', async () => {
+    const html = await $fetch<string>('/once-nav-initial')
+
+    expect(html).toContain('once.vue')
+    expect(html).toContain('once: 2')
+
+    const { page } = await renderPage('/once-nav-initial')
+    expect(await page.getByText('once:').textContent()).toContain('once: 2')
+  })
   it('`useId` should generate unique ids', async () => {
     // TODO: work around interesting Vue bug where async components are loaded in a different order on first import
     await $fetch<string>('/use-id')
diff --git a/test/fixtures/basic/pages/once-nav-initial.vue b/test/fixtures/basic/pages/once-nav-initial.vue
new file mode 100644
index 0000000000..475d2a4617
--- /dev/null
+++ b/test/fixtures/basic/pages/once-nav-initial.vue
@@ -0,0 +1,14 @@
+<script setup>
+const counter = useState('once', () => 0)
+
+await callOnce(() => counter.value++, { mode: 'navigation' })
+await callOnce('same-key', () => counter.value++, { mode: 'navigation' })
+await callOnce('same-key', () => counter.value++, { mode: 'navigation' })
+</script>
+
+<template>
+  <div>
+    <div>once.vue</div>
+    <div>once: {{ counter }}</div>
+  </div>
+</template>
diff --git a/test/nuxt/composables.test.ts b/test/nuxt/composables.test.ts
index 9d2a812335..00895555e5 100644
--- a/test/nuxt/composables.test.ts
+++ b/test/nuxt/composables.test.ts
@@ -1,6 +1,6 @@
 /// <reference path="../fixtures/basic/.nuxt/nuxt.d.ts" />
 
-import { describe, expect, it, vi } from 'vitest'
+import { afterEach, describe, expect, it, vi } from 'vitest'
 import { defineEventHandler } from 'h3'
 import { destr } from 'destr'
 
@@ -781,33 +781,55 @@ describe('useCookie', () => {
 })
 
 describe('callOnce', () => {
-  it('should only call composable once', async () => {
-    const fn = vi.fn()
-    const execute = () => callOnce(fn)
-    await execute()
-    await execute()
-    expect(fn).toHaveBeenCalledTimes(1)
-  })
+  describe.each([
+    ['without options', undefined],
+    ['with "render" option', { mode: 'render' as const }],
+    ['with "navigation" option', { mode: 'navigation' as const }],
+  ])('%s', (_name, options) => {
+    const nuxtApp = useNuxtApp()
+    afterEach(() => {
+      nuxtApp.payload.once.clear()
+    })
+    it('should only call composable once', async () => {
+      const fn = vi.fn()
+      const execute = () => options ? callOnce(fn, options) : callOnce(fn)
+      await execute()
+      await execute()
+      expect(fn).toHaveBeenCalledTimes(1)
+    })
 
-  it('should only call composable once when called in parallel', async () => {
-    const fn = vi.fn().mockImplementation(() => new Promise(resolve => setTimeout(resolve, 1)))
-    const execute = () => callOnce(fn)
-    await Promise.all([execute(), execute(), execute()])
-    expect(fn).toHaveBeenCalledTimes(1)
+    it('should only call composable once when called in parallel', async () => {
+      const fn = vi.fn().mockImplementation(() => new Promise(resolve => setTimeout(resolve, 1)))
+      const execute = () => options ? callOnce(fn, options) : callOnce(fn)
+      await Promise.all([execute(), execute(), execute()])
+      expect(fn).toHaveBeenCalledTimes(1)
 
-    const fnSync = vi.fn().mockImplementation(() => {})
-    const executeSync = () => callOnce(fnSync)
-    await Promise.all([executeSync(), executeSync(), executeSync()])
-    expect(fnSync).toHaveBeenCalledTimes(1)
-  })
+      const fnSync = vi.fn().mockImplementation(() => {})
+      const executeSync = () => options ? callOnce(fnSync, options) : callOnce(fnSync)
+      await Promise.all([executeSync(), executeSync(), executeSync()])
+      expect(fnSync).toHaveBeenCalledTimes(1)
+    })
 
-  it('should use key to dedupe', async () => {
-    const fn = vi.fn()
-    const execute = (key?: string) => callOnce(key, fn)
-    await execute('first')
-    await execute('first')
-    await execute('second')
-    expect(fn).toHaveBeenCalledTimes(2)
+    it('should use key to dedupe', async () => {
+      const fn = vi.fn()
+      const execute = (key?: string) => options ? callOnce(key, fn, options) : callOnce(key, fn)
+      await execute('first')
+      await execute('first')
+      await execute('second')
+      expect(fn).toHaveBeenCalledTimes(2)
+    })
+
+    it.runIf(options?.mode === 'navigation')('should rerun on navigation', async () => {
+      const fn = vi.fn()
+      const execute = () => options ? callOnce(fn, options) : callOnce(fn)
+      await execute()
+      await execute()
+      expect(fn).toHaveBeenCalledTimes(1)
+
+      await nuxtApp.callHook('page:start')
+      await execute()
+      expect(fn).toHaveBeenCalledTimes(2)
+    })
   })
 })