On this page:

5.7FHIR Performance and Caching


This page outlines technical considerations for setting up a performant FHIR repository.

5.7.1The Query Cache


Smile CDR uses an internal mechanism called the Search Coordinator, which executes database search queries in a background thread in order to minimize the amount of time that a client waits for search results to become available.

The following diagram shows the execution flow for a typical search.

Query Cache

When a search begins, a check is first made to determine whether or not the query has already recently occurred. If two search requests are made in close succession for the exact same search parameters, the results from the first search will be reused and returned for the second instead of executing the same search again. This can result in time savings as the query cache is very fast compared to executing searches against the database.

In addition, the query cache can prefetch more results than have been requested by the user. This is done in order to optimize page requests. For example, if a user requests all Patients named "smith", they may receive only the first 10 results on the single page. The Search Coordinator may continue fetching more than the first 10 results in the background. If the client attempts to load resources beyond the number that has been pre-fetched and stored in the Query Cache, the Search Coordinator will begin a new search in the database for further results.

Note that requests against the query cache will always respect security and authorization rules. If a particular search is not permitted for the calling user or if the search returns results that the calling user is not permitted to receive, the security manager will deny this request per normal behaviour.

Cache Misses

If the Search Coordinator determines that there is no appropriate search result already in the cache, a new search is executed.

As soon as a single page of results is available (the size of a page depends on the client request and the server default; however, it is typically a small number such as 50 results), these results are returned to the client. The server will continue fetching subsequent pages in the background, and these will be added to the query cache.

The search result returned to the client is a FHIR Bundle resource containing the page of results as well as a link that can be used to fetch the next page of results. If the client chooses to fetch the next page, these results will be retrieved from the query cache.

Cache Hits

If the Search Coordinator determines that search result already exists in the query cache (and it is not too stale to return), the existing results will be returned again.

The validity period for a search result is configurable (see below). By default a search result will remain eligible for reuse for 1 minute.

When receiving search results from the FHIR Endpoint, there are several hints that indicate that a cached search result was returned:

  • A special header called X-Cache will be returned, containing the value HIT and the identity of the FHIR Endpoint. For example:
X-Cache: HIT from https://fhir.acme.org:8000
  • The returned Bundle will have a lastUpdated time corresponding to the timestamp of the original search.

  • The Bundle will have an ID that corresponds to the cache entry ID. This is meaningless on its own but if two searches return a Bundle with the same ID then you will know that they came from the same entry in the query cache.

False Positives for Cache Hits

Use of the query cache does potentially mean that slightly out-of-date results can be returned. For example, consider the following scenario where all of these steps happen in rapid succession:

  1. A search for Patient?name=smith is performed.
  2. Then, a new patient named "smith" is created.
  3. A second search for Patient?name=smith is performed.

If the cache is set to 1 minute, and the above steps happen in less than 1 minute, the newly created patient from step 2 will not be reflected in the step 3 search results because the step 1 search results will be reused.

This is not an issue in most real-world scenarios so the benefit in performance is generally worth it; however, this should be considered when designing an application. If you are building a workflow that requires a search to reflect resources that were only just created, consider disabling the cache either globally or for the given request.

Controlling the Query Cache: Server

On the server, it is possible to adjust the default timeout for the query cache. If the dao_config.reuse_cached_results_timeout_millis property is set to 0 (zero), the query cache will never be used to respond to a new search. Note that this does not mean that nothing will be stored in the query cache, as subsequent page requests for a single search will still use the query cache.

If the dao_config.reuse_cached_results_timeout_millis property is set to a positive value, a search result in the query cache is eligible to be reused for the given number of milliseconds.

Controlling the Query Cache: Client

The client may request that a specific request skips the query cache by using the Cache-Control header.

By using this header with a value of no-cache, the client instructs the server that the query cache should not be considered.

For example, the following request searches for patients named "Smith" and will not use the query cache:

GET /Patient?name=smith
Cache-Control: no-cache
Accept: application/fhir+json

Performing no-store Queries: Client

In some circumstances, the client may want to perform a search that should return as quickly as possible and that does not require a large number of results.

This can be accomplished by using a no-store query, which avoids persisting search results in the query cache. This has several implications:

  • All found results will be returned immediately, and paging will not be used (or available) for this search.
  • Future client requests will not be able to reuse the results of this search.

The following example shows a client request that can only return up to 50 results (any matches beyond 50 are ignored), and should return as quickly as possible.

GET /Patient?name=smith
Cache-Control: no-cache, no-store, max-results=50
Accept: application/fhir+json

5.7.2Search Counts


When performing a FHIR search, one of the elements returned in the response Bundle is the Bundle.total element, which contains a count of how many resources match the given search.

If you are building a system which relies on this total, there are several points to consider:

The total may not be included on the first page

When performing a search that matches a large number of results, the first page of results may not have the Bundle.total element populated. This is because the Search Controller will attempt to return data as soon as it becomes available, even if the total number of matching results is not known. Put another way, by default the total number of search results is not always included in responses in order to improve performance. This behavior can be adjusted as shown below.

Returning just the total

If you are performing a search that requires only the count and does not actually require the corresponding data, you can avoid costly data loading and force a count to be loaded using the _summary parameter. The following example shows a query which counts all Patients born on/after January 01 1980. http://hapi.fhir.org/baseR4/Patient?birthdate=ge1980-01-01&_summary=count

This will return a result similar to the following:

  "resourceType": "Bundle",
  "id": "72282727-65f1-4f2f-8bb3-aa8721a6f729",
  "total": 29

Forcing a Total and Data

You can force a total count along with the accompanying data by using the _total parameter. This parameter can have any of the following values:

  • none: Do not attempt to calculate the total
  • estimated: Return an estimated total (not currently supported)
  • accurate: Always include the total

For example: http://hapi.fhir.org/baseR4/Patient?birthdate=gt1973-05-31&_total=accurate

5.7.3Improving Write Performance


In many scenarios large amounts of data needs to be written to the CDR (e.g. during initial backloads, or for update-heavy applications). The following considerations may be helpful in planning for high-volume write scenarios:

Smile CDR Settings

The following settings can be used to tune your system for the fastest write performance.

  • Disable deletes: If the Delete Enabled setting is turned off, a number of deletion checks can be skipped when writing data and an additional client-assigned ID cache is automatically enabled. This reduces the number of reads required during a resource create/update, especially if your resources have lots of references to other resources. It is worth considering disabling this setting (even if only temporarily) during bulk loading exercises.

  • Enabling Match URL Cache: If the Match URL Cache setting is enabled, the resolution of any conditional URLs used in your write operations (e.g. conditional create, conditional update) will be cached in an in-memory cache. This can improve overall write performance, especially in cases where conditional creates are frequently being used to resolve references to the same targets. For example, suppose you are uploading many ExplanationOfBenefit resources in a FHIR transaction bundle, and each one has a reference to a Patient and Practitioner resource, each one using a conditional create. Enabling this setting will avoid two lookups for each ExplanationOfBenefit. Note that this setting should not be used if your write patterns will change the targets of your conditional URLs (this is generally not the case, but should be considered).

  • Disable logs: The Audit Log and Transaction Log both require database processing, and add to the overall load during a write operation. Consider disabling one or both, especially during backloading activities.

  • Disable Unnecessary Features: The following features should be disabled (even if temporarily during backload) unless they are needed, as they add additional processing time for each resource being loaded.

  • Enable Mass Ingestion Mode: The Mass Ingestion Mode setting tunes the system to prioritize write operations over read operations.

  • Enable Write-Semaphore Mode: Write-Semaphore Mode avoids database contention by using an in-memory semaphore to avoid concurrent writes to the same resource. Note that this setting can decrease write performance if your workloads do not feature concurrent writes to the same resources, so it is a good idea to measure performance before and after enabling this setting.

  • Disable unnecessary search parameters: The FHIR specification describes a rich set of default Search Parameters for every resource type, and these are all enabled by default. Every enabled search parameter means additional processing work when a resource is written, so disabling search parameters that are not used can have a significant impact on write performance. See Search Parameter Tuning for more information.

Environment Preparation

Data Design

  • Use transactions: FHIR Transactions allow multiple operations to be batched into a single database transaction. Submitting multiple resources in a single transaction is almost always going to be faster than submitting them individually (i.e. each one in its own HTTP request), especially if those resources have references to each other. Note that you do not want to create transactions of unlimited size. The entire transaction bundle is loaded into memory during processing, so this is a practical limit to consider. Bundles containing hundreds or sometimes thousands are common.

    • Avoid Small FHIR Transactions: While FHIR transactions are a great tool for improving performance, a FHIR Transaction Bundle with a small number of entries (e.g. 1-2) will often perform slightly worse than equivalent individual operations due to the extra processing required to support placeholder ID resolution in transactions. It is often worth testing whether breaking up a transaction will yield better performance for your specific use case.
  • Avoid client assigned IDs: The FHIR "create with client assigned ID" uses an update/PUT operation to let the client control the ID of the resource inserted in the database, rather than relying on the server to assign one. This can be handy if you are replicating data from another system and want your IDs to match between systems. Client-assigned IDs come at a price however, as the system needs to first perform a read before every write to ensure that a resource doesn't already exist with the specified ID. It is often feasible to use the identifier field instead of the id field to store these source system IDs.