Blockscout supports running as a single combined application or as separate API and indexer instances via the APPLICATION_MODE environment variable.
APPLICATION_MODE environment variable (defined in config/config_helper.exs):
all(default) — both API and indexer run togetherapi— API-only instance, no indexingindexer— indexer-only instance, no API serving
The current mode is accessible via Explorer.mode() (defined in apps/explorer/lib/explorer.ex), which returns :all, :api, :indexer, or :media_worker.
:media_worker is a special standalone mode for NFT media processing. It is not set via APPLICATION_MODE — it activates when nft_media_handler[:standalone_media_worker?] is true, overriding the configured mode. In this mode, Explorer.Application starts only libcluster — no base_children or configurable_children. The process mode filtering rules below do not apply to :media_worker.
Related environment variables:
DISABLE_INDEXER— forces indexer off (auto-set whenAPPLICATION_MODE=api)DISABLE_API— forces API off (compile-time, controlsBlockScoutWeb.Endpoint)
When adding or modifying processes started by Explorer.Application, Indexer.Application, or BlockScoutWeb.Application, follow these rules:
Start in :indexer mode only:
- Active periodic updaters — GenServers that periodically query the DB and write results to
last_fetched_counterstable (e.g.,ContractsCount,NewPendingTransactionsCount,Transactions24hCount). The API side reads directly from the DB table without needing a local process. - Data migrators (
Explorer.Migrator.*) — one-time or ongoing data transformations. - Catalogers and tag importers (
AddressTag.Cataloger,CertifiedSmartContractCataloger). - Block gap scanning (
MinMissingBlockNumber). - Indexer-specific caches that are written and read by indexer only (
LatestL1BlockNumber).
Start in :api mode only:
- Passive on-demand ETS/in-memory caches — GenServers that manage an ETS table and populate it on API request (e.g.,
AddressTransactionsCount,TokenHoldersCount,BlockBurntFeeCount,AverageBlockTime). ETS is local to the process, so these must run on the instance serving requests. - On-demand fetchers triggered by API requests (
CheckBytecodeMatchingOnDemand,FetchValidatorInfoOnDemand,LookUpSmartContractSourcesOnDemand). - API access control (
AddressesBlacklist). - Contract verification tooling (
SolcDownloader,VyperDownloader). - Read-only DB replicas (
Explorer.Repo.Replica1). - API-only caches (
OptimismFinalizationPeriod,CeloEpochs,Rootstock.LockedBTCCount).
Start in both modes (:all, :api, :indexer):
- Core infrastructure: main
Explorer.Repo,Explorer.Vault,Registry.ChainEvents,Redix. - Event system:
Explorer.Chain.Events.Listener(mode-controlled via its own:enabledconfig). - Cluster discovery (
libcluster) — needed for node communication in separate mode.
configure(process)— starts ifApplication.get_env(:explorer, process)[:enabled] == true. No mode check.configure_mode_dependent_process(process, mode)— starts if:enabledis true ANDExplorer.mode()matches. Use for processes that have:enabledconfig inruntime.exs.only_in_mode(process, mode)— starts ifExplorer.mode()matches. No:enabledcheck. Use for processes without:enabledconfig (e.g., repos, downloaders, unconditional entries).configure_chain_type_dependent_process(process, chain_type)— starts if chain type matches. Can be piped with mode filters.
Piping pattern for combined restrictions:
SomeProcess
|> configure_mode_dependent_process(:indexer)
|> configure_chain_type_dependent_process(:optimism)How to distinguish active updaters from passive caches when deciding the mode:
- Active periodic updater: has
schedule_next_consolidation(),handle_info(:consolidate), writes tolast_fetched_countersviaLastFetchedCounter.upsert()->:indexer - Passive on-demand ETS cache: has
fetch()with cache expiry check, stores in ETS viaHelper.put_into_ets_cache(), may update model columns ->:api - MapCache (ConCache): uses
use Explorer.Chain.MapCache, implementshandle_fallback->:api