LlamaIndex Settings global vs per-object — LLM and embed_model resolution errors (2026) — Fix Guide (2026) | AI Error Hub
Home Providers LlamaIndex Settings resolution
LlamaIndex Config · Settings Severity: Medium HTTP n/a

LlamaIndex Settings — global vs per-object LLM and embed_model

<code>Settings</code> is LlamaIndex's global config singleton. It is convenient in scripts and a source of confusion in production apps where different indexes or requests want different models.

By Sana K. · Cloud AI Reliability Engineer Published 2026-07-26 Verified 2026-07-26

Quick fix (TL;DR)

Resolution: Settings.llm and Settings.embed_model are global defaults. They are used when you do not pass explicit llm= or embed_model= to a specific component. Fix by (a) always passing explicit models to critical components, (b) never mutating Settings mid-request in a multi-tenant app, and (c) treating Settings as an application-wide default only.

Real error messages you'll see

These are the exact strings returned by the LlamaIndex framework and its integrations when this error occurs. Copy-paste-searching any of them should land on this page.

Wrong LLM used mid-request
# Request 1: sets Settings.llm = OpenAI(gpt-4o)
# Request 2 concurrent: sets Settings.llm = Anthropic
# Request 1 continues; ends up calling Anthropic on a query it started with OpenAI
Async loop uses stale Settings
# Background task built index with Settings.embed_model = X
# Main thread later sets Settings.embed_model = Y
# Query uses Y; retrieval broken
Silent embed drift across processes
# Worker A ingests with default OpenAI embed
# Worker B ingests with an override; Settings.embed_model was changed
# Same index; two embedding spaces mixed

Reference

What Settings holds and how it is used

SettingPurposeDefault
Settings.llmDefault LLM for chat / completionOpenAI (env-var API key)
Settings.embed_modelDefault embedding modelOpenAI text-embedding-ada-002
Settings.node_parserDefault node parserSentenceSplitter (chunk=1024)
Settings.callback_managerDefault callback handlerEmpty
Settings.chunk_sizeDefault chunk_size for node parser1024
Settings.chunk_overlapDefault chunk_overlap20

Root causes, ranked by frequency

Based on developer reports across LlamaIndex forums, GitHub issues, and Discord community during 2025–2026.

  • 25%
    Concurrent requests mutating Settings. Two requests overwrite each other's config.
  • 18%
    Settings changed after object created. Index or engine captured old Settings; later mutation ignored.
  • 14%
    Different modules setting different defaults. Import order determines which wins.
  • 10%
    Test setup pollutes production Settings. Test suite mutates Settings; not reset.
  • 8%
    Missing embed_model at load time. load_index_from_storage uses whatever Settings says; not verified.
  • 7%
    Global side effects in libraries. Imported library mutates Settings on import.
  • 7%
    Multi-tenant config leakage. Tenant A's LLM used for tenant B.
  • 11%
    Async context confusion. Settings is not context-var-scoped; async tasks share it.

Fixes — copy-paste solutions

Fix #1

Always pass explicit models to critical components

The safest pattern — Settings is a default, not a source of truth.

For any index, query engine, agent, or chain that matters, pass llm= and embed_model= explicitly. Reserve Settings for convenience in scripts and prototypes.

explicit_models.py
from llama_index.core import VectorStoreIndex, Settings
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
from llama_index.llms.anthropic import Anthropic

# --- Bad: relies on global Settings ---
# Settings.llm = OpenAI(model="gpt-4o")
# Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
# index = VectorStoreIndex.from_documents(docs)
# query_engine = index.as_query_engine()

# --- Good: explicit models ---
embed_model = OpenAIEmbedding(model="text-embedding-3-small")
main_llm = OpenAI(model="gpt-4o-mini")

index = VectorStoreIndex.from_documents(
    docs,
    embed_model=embed_model,     # ← explicit
)

# Use different LLMs for different query engines
fast_engine = index.as_query_engine(
    llm=OpenAI(model="gpt-4o-mini"),
    embed_model=embed_model,
    similarity_top_k=6,
)

quality_engine = index.as_query_engine(
    llm=OpenAI(model="gpt-4o"),
    embed_model=embed_model,
    similarity_top_k=10,
    response_mode="tree_summarize",
)

# Per-tenant / per-request LLM
def get_engine_for_tenant(tenant_id: str, index):
    tenant_llm = load_tenant_llm(tenant_id)     # your logic
    return index.as_query_engine(
        llm=tenant_llm,
        embed_model=embed_model,
        similarity_top_k=6,
    )

# Multi-model comparison
alt_engine = index.as_query_engine(
    llm=Anthropic(model="claude-sonnet-5"),
    embed_model=embed_model,
)
Explicit models are three extra lines and eliminate a whole class of "which LLM ran this query?" debugging. Non-negotiable in multi-tenant apps.
Fix #2

Never mutate Settings mid-request in a multi-tenant app

Settings is a singleton — mutations affect every other request.

For multi-tenant apps, treat Settings as immutable after startup. Per-request config lives in the request handler, not in global state.

multi_tenant_safe.py
from fastapi import FastAPI
from llama_index.core import Settings
from llama_index.llms.openai import OpenAI
from llama_index.embeddings.openai import OpenAIEmbedding

app = FastAPI()

# --- App startup: set default Settings ONCE ---
@app.on_event("startup")
async def init_settings():
    Settings.llm = OpenAI(model="gpt-4o-mini")
    Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
    # Never mutated again after this

# --- Per-tenant config resolved per-request ---
def get_tenant_llm(tenant_id: str):
    # Look up tenant-specific LLM (API key, model choice)
    return OpenAI(
        api_key=fetch_tenant_key(tenant_id),
        model=fetch_tenant_model(tenant_id, default="gpt-4o-mini"),
    )

@app.post("/chat/{tenant_id}")
async def chat(tenant_id: str, message: str):
    # DO NOT: Settings.llm = get_tenant_llm(tenant_id)  ← global mutation
    # DO: use per-request llm explicitly
    tenant_llm = get_tenant_llm(tenant_id)
    tenant_index = load_tenant_index(tenant_id)
    query_engine = tenant_index.as_query_engine(llm=tenant_llm)
    return {"response": str(query_engine.query(message))}


# --- Multi-worker gunicorn / uvicorn ---
# Each worker has its OWN Settings singleton — so worker-level Settings.llm
# is fine, but do not mutate at request time.
Under load, Settings mutations from one request race with another's reads. The bug is intermittent and hard to reproduce — solve it by never mutating.
Fix #3

Reset Settings between tests

Test isolation prevents cross-test pollution.

Pytest fixtures should reset Settings state to a known baseline. Otherwise, a test that mutates Settings breaks later tests in ways depending on order.

test_isolation.py
# conftest.py — reset Settings state between tests
import pytest
from llama_index.core import Settings
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI

@pytest.fixture(autouse=True)
def reset_llamaindex_settings():
    """Snapshot Settings before each test, restore after."""
    snapshot = {
        "llm": Settings.llm,
        "embed_model": Settings.embed_model,
        "node_parser": Settings.node_parser,
        "chunk_size": Settings.chunk_size,
        "chunk_overlap": Settings.chunk_overlap,
    }
    try:
        yield
    finally:
        Settings.llm = snapshot["llm"]
        Settings.embed_model = snapshot["embed_model"]
        Settings.node_parser = snapshot["node_parser"]
        Settings.chunk_size = snapshot["chunk_size"]
        Settings.chunk_overlap = snapshot["chunk_overlap"]


# In a specific test that needs a custom LLM
def test_custom_llm(reset_llamaindex_settings):
    Settings.llm = OpenAI(model="gpt-3.5-turbo")
    # ... test logic ...
    # After the test, Settings is restored automatically


# For unit tests, prefer FakeLLM and MockEmbedding
from llama_index.core.llms.mock import MockLLM
from llama_index.core.embeddings.mock_embed_model import MockEmbedding

def test_query_engine_logic():
    mock_llm = MockLLM(max_tokens=100)
    mock_embed = MockEmbedding(embed_dim=1536)

    # Pass explicitly — never rely on Settings in tests
    index = VectorStoreIndex.from_documents(
        docs, llm=mock_llm, embed_model=mock_embed,
    )
    query_engine = index.as_query_engine(llm=mock_llm, embed_model=mock_embed)
    response = query_engine.query("test")
    assert response
The autouse fixture is the safest — it applies to every test without explicit opt-in. For CI where isolation matters more than convenience, add it.

Prevention checklist

Ship these seven safeguards once and this error stops appearing in your logs.

  • Pass llm= and embed_model= explicitly to every index, query engine, and agent.
  • Set Settings once at app startup and never mutate at request time.
  • For multi-tenant apps, resolve tenant-specific config per-request; do not put it in Settings.
  • Reset Settings between tests via a pytest fixture.
  • For async apps, be aware Settings is not context-var-scoped.
  • Avoid library imports that mutate Settings; audit third-party imports.
  • Use MockLLM and MockEmbedding in unit tests to avoid provider calls.

Frequently asked questions

ServiceContext is deprecated. Use Settings for global defaults, or pass models explicitly to components. Explicit passing is preferred for production.
Settings is a module-level singleton without lock protection. Reads and writes are Python-atomic for individual attributes but not composite. In multi-threaded async apps, treat it as effectively immutable after startup.
Deprecated in LlamaIndex 0.10 (early 2024); still exists as a stub but should not be used. New code uses Settings + explicit models exclusively.
No — Settings is a single module-level singleton. To get "per module" behavior, use explicit models and stop relying on Settings.
Between building the first and second index, something changed Settings.embed_model. Common causes: a library imported in between, a test setup, or code that dynamically swaps models. Always be explicit.

Get the weekly AI-error digest

New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.