Configuration: solrconfig.xml, the Config API & caches

This section documents the current Solr line (10.0; 9.10.x the maintained 9.x branch), written and verified against the Apache Solr Reference Guide. No specific patch version is pinned. Some capabilities (the Solr Operator on Kubernetes, the package-manager ecosystem, Learning To Rank model training, and expert plugin development) are linked, not documented in depth.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

solr.xml configures the node itself — ZooKeeper connection, the core/collection container, HTTP shard-handler pool sizing — and is read once at startup. solrconfig.xml configures one core/replica: request handlers, update processing, and the caches this page is about, and much of it can be changed live, either by editing the file and RELOAD`ing (see Collections API & configsets) or through the Config API, which writes an overlay without touching the file on disk. This page assumes the schema/configset layout from that page; it covers what actually lives inside `solrconfig.xml and how Solr’s query-time caches behave.

Property substitution

solrconfig.xml (and the schema) can reference ${name:default} placeholders, resolved at core load time from, in order of precedence, JVM system properties (-Dsolr.autoCommit.maxTime=15000), a core’s core.properties file, and the placeholder’s own :default fallback. This is what lets one configset be shared by collections that only differ in, say, an autocommit interval or a data directory:

<updateHandler class="solr.DirectUpdateHandler2">
  <autoCommit>
    <maxTime>${solr.autoCommit.maxTime:15000}</maxTime>
    <openSearcher>false</openSearcher>
  </autoCommit>
</updateHandler>

The Config API's set-user-property command (below) defines the same kind of variable at runtime, stored in ZooKeeper as configoverlay.json, without editing the configset at all.

The Config API

Editing solrconfig.xml and re-uploading the configset works, but every change needs a RELOAD. The Config API instead writes small, additive JSON commands into an overlay (configoverlay.json) layered on top of the file at load time — most changes apply without a full core reload:

# Change a known, predefined property (dot path into the effective config)
curl -X POST "http://localhost:8983/solr/books/config" -H 'Content-Type: application/json' -d '{
  "set-property": {"updateHandler.autoCommit.maxTime": 15000}
}'

# Add a new request handler
curl -X POST "http://localhost:8983/solr/books/config" -H 'Content-Type: application/json' -d '{
  "add-requesthandler": {
    "name": "/mypath",
    "class": "solr.SearchHandler",
    "defaults": {"rows": 10, "df": "text"}
  }
}'

# Define a user property for property substitution, then read the effective config back
curl -X POST "http://localhost:8983/solr/books/config" -H 'Content-Type: application/json' -d '{
  "set-user-property": {"my.custom.variable": "some_value"}
}'
curl "http://localhost:8983/solr/books/config/overlay"

set-property only accepts a fixed, predefined set of dotted property paths (cache sizes, autocommit settings, and similar); use add-requesthandler / update-requesthandler / delete-requesthandler (and the matching commands for search components, query parsers, and update processors) to manage whole components. See Config API for the full command reference, including the V2 (/api/collections/<collection>/config) equivalents.

Request-handler configuration: defaults, appends, invariants

A <requestHandler> block in solrconfig.xml (or one added via the Config API above) can carry three parameter blocks that combine with whatever the client sends, in increasing order of precedence:

  • defaults — used only when the client does not supply that parameter.

  • appends — added to the client’s value; used with multi-valued parameters such as fq, since it adds a value rather than replacing one.

  • invariants — always wins, even over a value the client explicitly sends; the standard way to pin a security- or tenant-relevant filter so a client cannot override it.

<requestHandler name="/select" class="solr.SearchHandler">
  <lst name="defaults">
    <str name="df">text</str>
    <int name="rows">10</int>
    <str name="wt">json</str>
  </lst>
  <lst name="appends">
    <str name="fq">in_stock:true</str>
  </lst>
  <lst name="invariants">
    <str name="fq">tenant:acme</str>
    <int name="rows">1000</int>
  </lst>
</requestHandler>

Here every request against /select is silently confined to tenant:acme no matter what the client passes, is additionally filtered to in_stock:true on top of any fq the client sends, and falls back to text / 10 / json only when the client is silent on those three. See Query basics & parameters for the parameters themselves, and Security for using invariants as a tenant- or row-level access boundary.

Caches

Every open IndexSearcher owns its own generation of Solr’s built-in caches; a new searcher starts with them empty (unless auto-warmed, below) and the old searcher’s caches are dropped once in-flight requests against it finish. All of them are sized and tuned per core in solrconfig.xml.

filterCache

Caches the document set (as a bitset) each distinct fq filter or \{!frange} clause matches, keyed by the filter itself, and reused verbatim by any other query using the same filter — the same role Elasticsearch’s node query cache plays for filter context. It is also what backs faceting on non-docValues fields internally in some code paths, so an undersized filterCache shows up as slow facets as well as slow filtered queries.

<filterCache class="solr.CaffeineCache"
              size="512"
              initialSize="512"
              autowarmCount="128"/>

queryResultCache

Caches the ordered list of document ids a full query (query string plus sort plus filters) produced, keyed on the whole request signature. A repeat of the same search, or a later page of it within queryResultWindowSize, is served from this cache instead of re-running Lucene collection. It stores ids only, not field data, so it is cheap per entry relative to documentCache.

<queryResultCache class="solr.CaffeineCache"
                   size="256"
                   initialSize="256"
                   autowarmCount="32"/>

documentCache

Caches the stored fields of individual Lucene documents (by internal doc id), so returning the same hit across several requests — including the overlapping ids between one page of results and the next — avoids repeat stored-field decompression. Unlike the two caches above, documentCache entries cannot be autowarmed (there is no autowarmCount): a new searcher always starts it cold, since the internal doc ids it is keyed on are not stable across searcher generations.

<documentCache class="solr.CaffeineCache"
                size="1024"
                initialSize="1024"/>

fieldValueCache

A per-segment cache (unlike the three whole-searcher caches above) of per-field, per-document value data, used mainly for faceting on multi-valued or non-docValues fields. Being per-segment means only the segments that actually changed since the last commit need rebuilding, rather than the entire cache, which matters on a large index with small, incremental commits.

<fieldValueCache class="solr.CaffeineCache"
                  size="128"
                  autowarmCount="0"/>

User-defined caches

Any additional <cache name="…​"> block with the same class/size/autowarmCount attributes declares an application-specific cache that a custom component can look up by name via SolrIndexSearcher.getCache. See Caches and Query Warming for the complete parameter list (maxRamMB, showItems, and eviction-policy details for the Caffeine and legacy LRU cache implementations).

Every cache exposes hit-rate and eviction metrics through the metrics API covered in Monitoring & metrics; a low filterCache or queryResultCache hit ratio there is usually the first sign a size/autowarmCount needs raising.

Autowarming and new-searcher warming

Because each searcher generation starts with empty caches, opening a new one after a commit can make the next request against it much slower than steady state — unless the new searcher is warmed before it is put into service:

  • Autowarming — autowarmCount on filterCache / queryResultCache re-runs (a bounded number of) the old searcher’s most-recently-used entries against the new searcher while it is being opened, so common filters and queries are already cached the moment it starts serving.

  • firstSearcher / newSearcher listeners — explicit warming queries configured under <listener event="firstSearcher" …​> (run once, on core startup, when there is no prior searcher to autowarm from) and <listener event="newSearcher" …​> (run on every commit thereafter), each firing a static list of representative queries.

<listener event="newSearcher" class="solr.QuerySenderListener">
  <arr name="queries">
    <lst><str name="q">*:*</str><str name="sort">last_modified desc</str></lst>
    <lst><str name="q">*:*</str><str name="fq">in_stock:true</str></lst>
  </arr>
</listener>
<listener event="firstSearcher" class="solr.QuerySenderListener">
  <arr name="queries">
    <lst><str name="q">static firstSearcher warming query</str></lst>
  </arr>
</listener>

Warming a new searcher takes time proportional to autowarmCount and the listener queries, during which the old searcher keeps serving live traffic — Solr does not block requests waiting for a commit’s new searcher unless useColdSearcher is set. maxWarmingSearchers bounds how many searchers may be warming concurrently; the default of 2 is deliberately low because each warming searcher holds index files open and duplicates cache-warming work, and a commit rate high enough to pile up more than a couple of them concurrently means autocommit is tuned too aggressively for the warming cost you configured, not that the limit should be raised:

<query>
  <maxWarmingSearchers>2</maxWarmingSearchers>
  <useColdSearcher>false</useColdSearcher>
</query>

With useColdSearcher false (the default), a request that arrives while every permitted warming slot is full simply keeps using the last fully-warmed searcher rather than the brand-new one, so warming pressure degrades freshness, never correctness. See Caches and Query Warming for the complete listener and warming reference, and Indexing internals & performance for how commit frequency and autoCommit/autoSoftCommit interact with how often a new searcher (and therefore warming) happens at all.

Circuit breakers

A circuit breaker rejects an incoming request outright, before it does any real work, when a node-level resource is already saturated — protecting the node from cascading failure rather than tuning how fast a request runs. Three built-in breakers ship with Solr, each configured as its own <circuitBreaker> block in solrconfig.xml:

<circuitBreaker class="solr.MemoryCircuitBreaker">
  <double name="threshold">75</double>
</circuitBreaker>

<circuitBreaker class="solr.CPUCircuitBreaker">
  <double name="threshold">75</double>
  <str name="requestTypes">query,update</str>
</circuitBreaker>

<circuitBreaker class="solr.LoadAverageCircuitBreaker">
  <double name="threshold">8.0</double>
</circuitBreaker>

MemoryCircuitBreaker trips on JVM heap usage as a percentage of max heap (valid range 50—​95%), CPUCircuitBreaker on system CPU utilization read from JMX, and LoadAverageCircuitBreaker on the OS load average — all three accept a requestTypes restriction (query, update, or both) and a warnOnly flag that logs a would-have-tripped event instead of actually rejecting requests, useful for calibrating a threshold before enforcing it. A tripped breaker returns HTTP 503 immediately, which is cheaper for both the node and the caller than letting the request queue behind an already overloaded resource. See Circuit Breakers for the complete list and interaction with distributed requests (a breaker trips per-node, so a distributed query can still partially succeed against the nodes that were not tripped).

Request rate limiters

Where a circuit breaker reacts to resource pressure, a request rate limiter enforces a fixed concurrency budget per request type, independent of how loaded the node currently looks — configured cluster-wide via the /api/cluster endpoint rather than per-core in solrconfig.xml:

curl -X POST "http://localhost:8983/api/cluster" -H 'Content-Type: application/json' -d '{
  "set-ratelimiter": {
    "enabled": true,
    "guaranteedSlots": 5,
    "allowedRequests": 20,
    "slotBorrowingEnabled": true,
    "slotAcquisitionTimeoutInMS": 70
  }
}'

# Inspect the active configuration
curl "http://localhost:8983/api/cluster"

allowedRequests bounds concurrent in-flight requests of that type (default: number of cores times 3); a request beyond the budget either waits up to slotAcquisitionTimeoutInMS for a slot to free up (-1, the default, means do not wait) or, once the timeout elapses, is rejected with HTTP 429. guaranteedSlots reserves a floor of slots for the request type regardless of load, and slotBorrowingEnabled lets one request type borrow another’s unused slots rather than sitting idle while the other type is quiet. See Request Rate Limiters for the query- vs. index-specific limiter settings and metrics exposed for each.

Rate limiters and circuit breakers are complementary, not redundant: a rate limiter caps how many requests of a type run at once regardless of resource state, while a circuit breaker rejects requests only once a resource is actually under pressure — run both, since a low but steady concurrency budget will not, by itself, stop a single expensive query from spiking heap or CPU past what the breaker is watching for.

See also

  • Collections API & configsets — how a configset carrying this solrconfig.xml is uploaded, shared across collections, and reloaded.

  • Indexing internals & performance — commit and merge tuning, the other half of what drives how often caches are warmed.

  • Monitoring & metrics — reading cache hit ratios, circuit-breaker trip counts, and rate-limiter rejections back out of a running node.

  • Security — using invariants as an access-control boundary, and the trust model for configsets that ship custom request handlers.