Guide
Diagnosing Turbo Drive Navigation Failures in Hotwire Applications
A step-by-step diagnostic guide for Turbo Drive issues: full reloads, scroll resets, stale caches, missing Turbo Stream responses, and third-party script re-initialization. Includes cause tables, verification commands, and escalation criteria.
Published by Tasadduq Burney
19 Nov 2025, 06:21 UTC
5 min26K views0

Recognizable Conditions
Turbo Drive replaces full page loads with fetch-based navigation, but several failure modes cause the browser to fall back to traditional navigation or behave unexpectedly. The most common symptoms are:
- Full browser reload instead of Turbo navigation
- Scroll position resets after navigation
- Form submissions fall back to full POST redirect
- Cached pages show stale data after mutations
- Third-party scripts re-initialize on every visit
- Redirects lose flash messages or session data
- Mobile Safari back/forward cache (bfcache) conflicts with Turbo Cache
Cause and Diagnostic Table
| Symptom | Likely Cause | Quick Diagnostic |
|---|---|---|
| Full browser reload | Missing data-turbo="false" on links/forms, CSP blocking turbo:visit events, or JS errors halting Turbo listeners |
Open DevTools Console: look for turbo:click/turbo:before-visit events. Network tab: check for Turbo-Visit request header. |
| Scroll position resets | Turbo Drive caches pages by default; data-turbo-action="replace" or advance changes history behavior. Turbo.cache.exemptPageFromCache() called or turbo:before-cache listeners mutate DOM incorrectly. |
Inspect Turbo.cache.has(location.href) and verify no turbo:before-cache handlers alter scroll state. |
| Form falls back to full POST redirect | Missing Accept: text/vnd.turbo-stream.html header, server responds with HTML instead of Turbo Stream, or data-turbo="false" on form. |
Network tab: check response Content-Type and presence of Turbo-Stream header. |
| Stale data after mutation | Turbo Cache retains previous body; missing turbo:before-cache cleanup or server not sending Vary: Turbo-Visit/Cache-Control: no-store for personalized content. |
Run Turbo.clearCache() after mutation or add data-turbo-temporary to fragments; verify cache size via Turbo.cache.size. |
| Third-party JS re-initializes | Scripts in <head> execute once, but body scripts re-run on each visit. |
Move initialization to turbo:load event or add data-turbo-eval="false" to script tags. Ensure Stimulus controllers use connect()/disconnect(). |
| Redirect loses flash/session | Turbo follows redirects automatically but discards response body if not Turbo Stream. Server must return 303 with Turbo Stream or use turbo:before-visit to preserve flash via sessionStorage. |
Check Turbo.session.drive configuration and response headers for Turbo-Location or Turbo-Redirect-To. |
| Mobile Safari bfcache conflict | Both caches restore pages differently; Turbo may restore stale DOM while bfcache restores JS state. | Add pagehide/pageshow listeners to call Turbo.cache.clear() or set Cache-Control: no-store on HTML responses. Test on real iOS Safari. |
Ordered Diagnostic Checks
- Verify Turbo Drive is enabled: In console, run
Turbo.session.drive. Iffalse, Drive is disabled globally. - Inspect a navigation request: Click a Turbo link. In Network tab, confirm the request includes
Turbo-Visit: trueheader and the response includesTurbo-LocationorTurbo-Redirect-To. - Trace visit lifecycle: Add
document.addEventListener('turbo:visit', e => console.log(e.detail))to see visitaction(advance,replace,restore). - Check cache state: Run
Turbo.cache.sizeandTurbo.cache.has(location.href)to confirm caching behavior. - Test with Drive disabled: Temporarily set
Turbo.session.drive = falseto isolate Turbo Drive vs. server issues. - Validate server responses: Use curl to simulate Turbo requests:
Should return Turbo Stream format (curl -H 'Accept: text/vnd.turbo-stream.html' -H 'Turbo-Visit: true' <url>Content-Type: text/vnd.turbo-stream.html).
Fixes Tied to Findings
Full Reload
- Ensure no
data-turbo="false"on links/forms unless intentional. - Check CSP:
script-srcmust allow'unsafe-inline'or nonces for Turbo's inline handlers unless using custom delegate viaTurbo.start(). - Fix any JavaScript errors that stop Turbo's event listeners (see console).
Scroll Reset
- Avoid calling
Turbo.cache.exemptPageFromCache()unless necessary. - Audit
turbo:before-cachelisteners: they must not mutate DOM in ways that lose scroll position. - Use
data-turbo-action="advance"(default) for normal navigation;replaceonly when you want to replace history entry.
Form Fallback
- Server must respond with
Content-Type: text/vnd.turbo-stream.htmlfor Turbo Stream responses. - Ensure the form does not have
data-turbo="false". - Verify the request includes
Accept: text/vnd.turbo-stream.html(Turbo adds this automatically).
Stale Cache
- Call
Turbo.clearCache()after mutations that affect cached pages. - Add
data-turbo-temporaryto fragments that should not be cached. - For personalized content, send
Vary: Turbo-VisitandCache-Control: no-storefrom the server.
Third-Party Script Re-initialization
- Move initialization code into a
turbo:loadlistener. - Add
data-turbo-eval="false"to script tags that should not be re-evaluated. - Stimulus controllers: use
connect()for setup anddisconnect()for teardown.
Redirect Flash Loss
- Return a Turbo Stream response with a
turbo-streamaction that updates the flash container. - Alternatively, store flash in
sessionStorageviaturbo:before-visitand restore onturbo:load.
Mobile Safari bfcache Conflict
- Add listeners:
window.addEventListener('pagehide', () => Turbo.cache.clear()); window.addEventListener('pageshow', (event) => { if (event.persisted) Turbo.cache.clear(); }); - Or set
Cache-Control: no-storeon HTML responses (impacts performance). - Always test on real iOS Safari; desktop responsive mode does not replicate bfcache behavior.
Escalation Criteria
Escalate when:
Turbo.session.drive = falseis set globally and cannot be changed.turbo:visiterrors persist after verifying CSP, headers, and JavaScript errors.- Turbo 8+ cache behavior changes (see Version Notes) cause regressions that cannot be resolved with configuration.
- Consider disabling Turbo Drive for specific routes via
data-turbo-action="replace"or opting out of Drive entirely for problematic pages.
Version Notes and Limitations
- Turbo 8 (2024) changed cache defaults:
Turbo.cachenow usesPageSnapshotandCacheObserver. Olderturbo:before-cachepatterns may behave differently. Verify migration guide if upgrading. - CSP
script-srcmust allow'unsafe-inline'or nonces for Turbo's inline event handlers unless using a custom delegate. - Turbo Streams require
Content-Type: text/vnd.turbo-stream.html; plain HTML responses to fetch/XHR will not trigger Stream actions. - Do not mix Turbo Drive with legacy
turbolinks; they share the globalTurbonamespace and conflict. - Mobile browsers may aggressively bfcache; test on real iOS Safari, not just desktop responsive mode.
Practical Verification Checklist
- DevTools Network: click a Turbo link → verify
Turbo-Visit: truerequest header andTurbo-Location/Turbo-Redirect-Toresponse headers. - Console:
Turbo.navigator.currentVisitduring navigation to inspect visit state andaction. - Console:
Turbo.cache.sizeandTurbo.cache.has(location.href)to confirm caching. - Console:
document.addEventListener('turbo:visit', e => console.log(e.detail))to trace lifecycle. - Temporarily set
Turbo.session.drive = falseto isolate Drive vs. server issues. - Curl test:
curl -H 'Accept: text/vnd.turbo-stream.html' -H 'Turbo-Visit: true' <url>returns Turbo Stream format.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.