<!-- llms-explorer concept facts · https://llms-explorer.com/tree/replanning-loops-and-replanreason/ · pack 2026-10-20 · ~5069 tokens -->

# Replanning loops and replanReason

> Source-anchored research for Replanning loops and replanReason. Run status: COMPLETED.

7 facets · 37 facts · page: https://llms-explorer.com/tree/replanning-loops-and-replanreason/

## Definitions

- Two other replanReason forms exist. 'cached plan returned: <status>' means the cached plan failed during its trial, for example with QueryExceededMemoryLimitNoDiskUseAllowed on a blocking sort. In that case the server replans but neither deactivates the cache entry nor caches the replanned winner. The SBE path also uses 'Foreign collection <ns> is not eligible for hash join anymore'. That form replans without running a trial and allows the new winner to be cached. (high confidence) (verified as of 2026-10-20) — [source](https://github.com/mongodb/mongo/blob/master/src/mongo/db/exec/classic/cached_plan.cpp)

## Structure and components

- The manual says planningTimeMicros (new in 6.2) is not a clean measure of replanning cost. Before 7.0.22, 8.0.5 and 8.1.0 it can include non-planning work such as a sharding metadata refresh. It can also include cached-plan trial-run time even when the query does not replan. The manual says not to rely on the field alone. SERVER-92476 fixed the non-planning part; SERVER-106502 (Backlog) covers the trial-run part. (high confidence) (verified as of 2026-10-19) — [source](https://www.mongodb.com/docs/manual/reference/database-profiler/)

## How it works

- If a query shape has an Active plan cache entry, the server runs a bounded trial of the cached plan before it uses the plan. If the trial costs more than internalQueryCacheEvictionRatio times the cost recorded when the plan was cached, the server deactivates the cache entry and plans the query again from scratch. MongoDB calls this second planning pass replanning. (high confidence) — [source](https://github.com/mongodb/mongo/blob/master/src/mongo/db/exec/classic/cached_plan.cpp)
- The Database Profiler Output reference defines two fields. replanned is a boolean that means the query system evicted a cached plan and re-evaluated all candidate plans. replanReason is a string that gives the specific reason a cached plan was evicted. Both appear only when replanned is true. In the server code, replanned is emitted only when replanReason is set. (high confidence) — [source](https://www.mongodb.com/docs/manual/reference/database-profiler/)
- Plan cache entries are Active or Inactive. A new entry starts Inactive, and the planner does not use it. A later planning run for the same shape promotes the entry to Active when that run's works are less than or equal to the stored works. If the new run's works are higher, the entry stays Inactive and its stored works grow by at least 1, by internalQueryCacheWorksGrowthCoefficient (default 2.0, so they double). SERVER-32452 introduced this design in 4.1.1, which shipped in 4.2. (high confidence) (verified as of 2026-10-20) — [source](https://www.mongodb.com/docs/manual/core/query-plans/)
- Derived from the budget formula: the trigger is relative to the cached cost, so an entry created from a very selective value is the easiest to overrun. Expected 2 works gives a budget of 20 works. Expected 7 reads gives a budget of 70 reads. A parameter value that costs far more than the creating value overruns the budget unless the plan first returns the trial result quota (default 101 results). (medium confidence) — [source](https://github.com/mongodb/mongo/blob/master/src/mongo/db/exec/classic/cached_plan.cpp)
- Percona lists the conditions that cause continuous replanning: data-dependent queries, unevenly distributed indexed values, indexes that are not selective enough, two or more plans with near-equal cost, and fast-changing cardinality. SERVER-79039 confirms the parameter-dependent case from the engine side. (medium confidence) — [source](https://www.percona.com/blog/mongodb-query-plan-cache-explained-performance-pitfalls-and-re-planning/)
- Percona also names mass inserts and deletes, TTL deletes, capped-collection overwrites, and $or, $in or $regex lists whose length varies between calls as triggers. It says write-heavy workloads are more sensitive than read-heavy ones. Percona gives no measurements for these causes. (low confidence) — [source](https://www.percona.com/blog/mongodb-query-plan-cache-explained-performance-pitfalls-and-re-planning/)
- Percona says that when every query invalidates the cached plan, replanning runs continuously and CPU use rises noticeably. SERVER-87572 states that 'high replanning rates may be associated with performance problems'. Neither source gives a rate above which replanning is harmful. (medium confidence) — [source](https://www.percona.com/blog/mongodb-query-plan-cache-explained-performance-pitfalls-and-re-planning/)
- Forum posters report replanReason entries with planningTimeMicros of 34 to 48 seconds, including one entry that returned zero documents. One reply blamed a huge number of indexes. It advised reviewing indexStats, removing unused or duplicate indexes, and hiding an index before dropping it. The thread has no accepted answer and no confirmation, so treat the cause as unverified. (low confidence) — [source](https://www.mongodb.com/community/forums/t/super-long-planningtimemicros-value/274072)
- MongoDB declined two requests to change the Inactive-entry rule and added diagnostics instead. SERVER-79039 (Won't Do, 2023-08-17) concerned the evict-replan cycle. SERVER-106395 (Won't Fix, 2025-06-18) asked that a new plan become Active at once when it performs about as well as the Inactive entry. The fetched SERVER-106395 page shows no rationale. SERVER-87572 and SERVER-98987 added the serverStatus replan counters. (medium confidence) (verified as of 2026-10-20) — [source](https://jira.mongodb.org/browse/SERVER-79039)

## Measurements and reference values

- internalQueryCacheEvictionRatio is a double with default 10.0 and validator gte 0.0, settable at startup and at runtime. Its IDL description is: 'How many times more works must we perform in order to justify plan cache eviction and replanning?' The plan cache README and SERVER-79039 both state the default as 10x. (high confidence) (verified as of 2026-10-20) — [source](https://github.com/mongodb/mongo/blob/master/src/mongo/db/query/query_optimization_knobs.idl)
- MongoDB engineers described the evict-replan-recache cycle in SERVER-79039 (created 2023-07-17). For some query patterns the cached plan's cost depends on the parameter values. A plan fitted to cheap values trips the 10x check on expensive values and is deactivated. A later cheap run can reactivate the Inactive entry because its works are lower, and the cycle restarts. MongoDB closed the ticket as Won't Do on 2023-08-17. (high confidence) — [source](https://jira.mongodb.org/browse/SERVER-79039)
- Replanning behavior changed across releases. In 3.2 through 4.0 a plan cached with an extremely high works value could stay cached and never replan; SERVER-32452 fixed this with Inactive entries in 4.1.1. SBE once replanned much more readily than the classic engine, which replans at about 10x the expected works; SERVER-59060 fixed that in 5.1.0-rc0. Since 8.0.0-rc0 the SBE path uses the same ratio-based read budget (SERVER-85238). (medium confidence) — [source](https://jira.mongodb.org/browse/SERVER-32452)

## Problems, failure modes and limitations

- The phrase 'replanning loop' names three different things. Percona uses it for a performance pathology in which every query invalidates the cached plan. SERVER-79039 calls it a 'cycle of cache->evict->replan->cache' for one query shape. The server code also has a bounded retry loop, retryMakePlanner, that re-runs planning after a ReplanningRequired error. Keep the three apart when you read a source. (high confidence) — [source](https://www.percona.com/blog/mongodb-query-plan-cache-explained-performance-pitfalls-and-re-planning/)
- In the classic engine, the replan budget is maxWorksBeforeReplan = (size_t)(internalQueryCacheEvictionRatio x decisionWorks), where decisionWorks is the works value stored in the cache entry. The trial ends without a replan if the cached plan returns the trial result quota or reaches EOF inside the budget. The quota is internalQueryPlanEvaluationMaxResults (default 101), lowered to the query's limit when the limit is smaller. If the plan spends the whole budget first, the stage replans. (high confidence) (verified as of 2026-10-20) — [source](https://github.com/mongodb/mongo/blob/master/src/mongo/db/exec/classic/cached_plan.cpp)
- SERVER-18468 added fromMultiPlanner and replanned to slow-query log lines and system.profile entries. Its fix versions are 3.2.5 and 3.3.2. Each field appears only when its value is true. The ticket page does not mention replanReason, so the release that added that field is not established here. (low confidence) — [source](https://jira.mongodb.org/browse/SERVER-18468)
- Slow-query log entries (log id 51803, msg 'Slow query') and system.profile entries carry the fields needed to triage replans: replanned, replanReason, fromMultiPlanner, planningTimeMicros, planCacheShapeHash and planCacheKey. Group by planCacheShapeHash to find one shape that replans repeatedly. planCacheKey changes when indexes that can support the shape are added or dropped, but planCacheShapeHash does not. From 8.0, planCacheShapeHash duplicates the older queryHash field. (high confidence) (verified as of 2026-10-19) — [source](https://www.mongodb.com/docs/manual/reference/database-profiler/)
- The Query Plans manual page says explain ignores all existing plan cache entries and does not create new ones. An explain run therefore does not execute the cached-plan trial. Observe replans through the profiler, the slow-query log and the serverStatus counters instead. (medium confidence) (verified as of 2026-10-14) — [source](https://www.mongodb.com/docs/manual/core/query-plans/)
- Any DDL on a collection (drop, create index, drop index, hide index) empties that collection's plan cache. A mongod restart and LRU eviction also remove entries. After a flush, many same-shape queries can multi-plan at once. The MongoDB plan cache README calls this a multiplanning storm and warns of out-of-memory risk for shapes with a multikey index scan or a blocking sort (mitigation tracked in SPM-3900). Clearing the cache by hand to stop a replan loop is therefore a temporary reset with its own risk. (medium confidence) — [source](https://www.mongodb.com/docs/manual/core/query-plans/)
- Blanket hints trade away the planner's ability to adapt. In Close's 2015 incident (MongoDB 2.6 era, before inactive entries existed), emergency hard-coded index hints restored most queries but mishandled edge cases and caused a second slowdown. Engineers later reverted many hints. Netdata notes that hints bypass the planner, so a hinted query fails if the hinted index cannot support it. Percona says hint() in production is not ideal but can stabilize a pathological case. (medium confidence) — [source](https://status.close.com/incidents/90l5pvdh1n7d)

## Comparisons and alternatives

- In the SBE path, the server counts reads instead of works. Reads equal totalKeysExamined plus totalDocsExamined. The budget is internalQueryCacheEvictionRatio times the winning plan's reads from multi-planning (decisionReads). A TrialRunTracker ends the trial when the read budget is exceeded. SERVER-85238 added this algorithm for find queries; its fix version is 8.0.0-rc0. (high confidence) — [source](https://jira.mongodb.org/browse/SERVER-85238)
- For an SBE budget overrun the same sentence says reads instead of works. The 'at least' number is the read count the tracker had observed when it stopped the trial. A community log shows 'expected trial execution to take 7 reads but it took at least 71 reads', which fits a 10x budget of 70 reads. (high confidence) (verified as of 2026-10-20) — [source](https://github.com/mongodb/mongo/blob/master/src/mongo/db/exec/runtime_planners/classic_runtime_planner_for_sbe/cached_planner.cpp)
- A budget-overrun replan deactivates the cache entry instead of deleting it. The entry keeps its works value. The code comment gives the reason: plans that perform even worse than the cached one should not easily take its place. If internalQueryCacheDisableInactiveEntries is true (default false), deactivation does nothing and every cache write is Active. (high confidence) (verified as of 2026-10-20) — [source](https://github.com/mongodb/mongo/blob/master/src/mongo/db/query/plan_cache/plan_cache.h)
- A server test reproduces the oscillation. It builds two indexes, {a:1} and {b:1}, and one query shape. It alternates a query that suits {a:1} with a query that suits {b:1}. Each switch replans, and the classic replanned counter rises by one per switch. The new plan differs from the cached plan, so replanned_plan_is_cached_plan stays flat. A query that is slow on both indexes then raises the Inactive entry's works to twice the old value. (high confidence) — [source](https://github.com/mongodb/mongo/blob/master/jstests/noPassthroughWithMongod/query/plan_cache_replanning.js)
- Do not treat planningTimeMicros close to durationMillis as proof that replanning caused the delay. Percona reads a sample with planningTimeMicros 215107053 and durationMillis 215107 as time spent in planning. The sample also shows numYields 15603, nreturned 0 and a replanReason of 2 works versus 20 works. On versions before 7.0.22, 8.0.5 and 8.1.0 the field may include trial-run execution and other non-planning work. (medium confidence) (verified as of 2026-10-19) — [source](https://www.percona.com/blog/mongodb-query-plan-cache-explained-performance-pitfalls-and-re-planning/)

## Facts and statements

- For a classic budget overrun, replanReason reads 'cached plan was less efficient than expected: expected trial execution to take N works but it took at least M works'. N is the works value stored in the cache entry. M is the budget, which is the eviction ratio times N, truncated to an integer. A Percona log sample shows N=2 and M=20. (high confidence) (verified as of 2026-10-20) — [source](https://github.com/mongodb/mongo/blob/master/src/mongo/db/exec/classic/cached_plan.cpp)
- After a classic cached plan returns a ReplanningRequired status, retryMakePlanner catches it. It stores replanReason, the cache mode and the old plan hash, then runs the full planning path again without consulting the plan cache. The loop allows at most kMaxIterations = 5 passes. After that it raises tassert 8712800 with the text 'Exceeded retry iterations for making a planner'. The same loop also handles NoDistinctScansForDistinctEligibleQuery, NoQueryExecutionPlans and RetryMultiPlanning. (high confidence) (verified as of 2026-10-20) — [source](https://github.com/mongodb/mongo/blob/master/src/mongo/db/query/get_executor_helpers.cpp)
- The SBE classic-runtime planner replans inside its own cached planner. Its replan function plans the whole query again. It builds a single-solution pass-through planner if planning yields one solution. Otherwise it builds a MultiPlanner that carries the replanReason. If the deferred engine-choice feature flag is on, it throws ReplanningRequired instead, so the same retry loop handles it. A source comment says only classic planning throws that exception today (TODO SERVER-120501). (medium confidence) (verified as of 2026-10-20) — [source](https://github.com/mongodb/mongo/blob/master/src/mongo/db/exec/runtime_planners/classic_runtime_planner_for_sbe/cached_planner.cpp)
- A replan can choose the same plan that was cached. In the server test, one index suits every value. A cheap value (1 match) creates the entry, and an expensive value (990 matches) overruns the budget. The expensive query replans, picks the same index plan, and leaves the entry Inactive. The next cheap query reactivates the entry without a replan. The server counts this case in replanned_plan_is_cached_plan. SERVER-98987 calls it wasteful replanning. (high confidence) — [source](https://github.com/mongodb/mongo/blob/master/jstests/noPassthroughWithMongod/query/plan_cache_replanned_plan_is_cached_plan.js)
- Some cache entries are never replanned. SBE pinned entries (single-solution queries and rooted $or queries) store no works or reads value, so no cached-plan trial runs. In the SBE cached planner, a missing decisionReads value sends the plan straight to a pass-through planner. Rooted $or queries use the cache per branch, and MongoDB does not replan or evict subplanned queries (SERVER-18777). A classic single-solution query creates no cache entry. (high confidence) (verified as of 2026-10-20) — [source](https://github.com/mongodb/mongo/blob/master/src/mongo/db/query/plan_cache/README.md)
- serverStatus reports plan cache counters under metrics.query.planCache.classic and metrics.query.planCache.sbe. The fields are hits, misses, replanned, replanned_plan_is_cached_plan and skipped. replanned counts plans that were discarded and re-optimized. replanned_plan_is_cached_plan counts replans that produced a plan identical to one already in the cache. SERVER-87572 added the replanning counter (fix versions 8.1.0-rc0, 8.0.5, 7.0.22). SERVER-98987 (fix version 8.2.0-rc0) added the counter for wasteful replans. (high confidence) (verified as of 2026-10-14) — [source](https://www.mongodb.com/docs/manual/reference/command/serverStatus/)
- Three internal planner parameters govern the loop. All can be set at startup and at runtime. internalQueryCacheEvictionRatio is a double, default 10.0. internalQueryCacheWorksGrowthCoefficient is a double, default 2.0, and must be greater than 1.0. internalQueryCacheDisableInactiveEntries is a boolean, default false. The Query Plans manual page does not mention any of them. (medium confidence) (verified as of 2026-10-20) — [source](https://github.com/mongodb/mongo/blob/master/src/mongo/db/query/query_optimization_knobs.idl)
- For pipelines that use $lookup, the plan-summary accumulator deliberately skips replanReason, fromMultiPlanner and planSelectionStrategy from the foreign side. The reported replanReason therefore describes only the local (left-hand) collection. (low confidence) (verified as of 2026-10-20) — [source](https://github.com/mongodb/mongo/blob/master/src/mongo/db/query/plan_summary_stats_visitor.h)
- Suggested triage, which synthesizes the sources: (1) Filter the mongod log for id 51803 with replanned true, or query system.profile for replanned: true. (2) Group by planCacheShapeHash and planCacheKey to find shapes that replan repeatedly. (3) Read replanReason for the expected and observed cost. A small expected number means the entry came from a very cheap value. (4) Compare serverStatus deltas of replanned and replanned_plan_is_cached_plan. A high ratio means the replans keep the same plan. (5) List the shape's cache entry with $planCacheStats or getPlanCache().list() and read isActive and works. (medium confidence) — [source](https://www.percona.com/blog/mongodb-query-plan-cache-explained-performance-pitfalls-and-re-planning/)
- Stop-gaps for a looping shape, from least to most invasive: reshape indexes so one plan wins for all parameter values; narrow or split long $or and $in lists; use hint() in extreme cases; on 8.0 and later, pin an index per query shape for the whole cluster with query settings. SERVER-87572 names hints and persistent query settings as workarounds for replanning-related slowdowns. SERVER-32452 lists hints, index filters and indexing strategy as workarounds for the opposite problem, a stuck bad plan. (medium confidence) — [source](https://www.percona.com/blog/mongodb-query-plan-cache-explained-performance-pitfalls-and-re-planning/)
- An engine test adapted from a repro for [redacted] covers a cached winner that hit EOF at once with 0 keys and 0 docs examined, so its stored decisionReads is 0. The test inserts hundreds of matching documents and asserts that the next run replans with a 'cached plan was less efficient than expected' reason and picks a cheaper index. The ticket itself needs a Jira login and was not read. (low confidence) — [source](https://github.com/mongodb/mongo/blob/master/jstests/noPassthrough/query/plan_cache/plan_cache_replan_decisionReads_is_zero.js)
