Documentsapisix
Plugin Configuration Inheritance
Plugin Configuration Inheritance
Type
Topic
Status
Published
Created
Jul 15, 2026
Updated
Jul 15, 2026

Plugin Configuration Inheritance#

APISIX merges plugin configurations from up to five entity levels into a single effective plugin set for each request. The merging precedence order, from highest to lowest, is:

Consumer > Consumer Group > Route > Plugin Config > Service

This means if the same plugin is configured on both a route and a service, the route's configuration wins. If a consumer also has that plugin, the consumer's configuration wins over all others.

Each non-global plugin executes once per request using the winning configuration — APISIX does not run multiple copies of the same plugin for different levels. Global rules (plugins attached via the global rules resource) are an exception: they always execute in addition to locally-bound plugins, even if the same plugin name appears in both.


How Each Level Is Merged#

Merging happens in the HTTP access phase inside http_access_phase() in apisix/init.lua, in three sequential steps:

1. Plugin-Config → Route (earliest, lowest precedence among "local" levels)#

If a route carries a plugin_config_id, plugin_config.merge(route, conf) is called first. The merge is additive-only: a plugin from the plugin-config is copied into the route's plugin table only if that plugin name is not already present in the route. Route-level plugins therefore always override plugin-config plugins with the same name.

The function is idempotent: it keeps a backup of the route's original plugins (orig_plugins) and restores it before re-applying on version change, preventing accumulation across hot-reload cycles.

2. Service → Route (HTTP routes only)#

plugin.merge_service_route(service, route) starts from a deep-copy of the service and overwrites individual plugin entries with route values. Route plugins always win over service plugins when the same name appears on both.

Stream routes differ: merge_service_stream_route starts from the route and only fills in service plugins that are not already present — the same additive-only pattern as plugin-config.

3. Consumer (+ Consumer Group) → Route (latest, highest precedence)#

After the rewrite phase runs, if an auth plugin identified a consumer (api_ctx.consumer), plugin.merge_consumer_route(route, consumer_conf, group_conf, api_ctx) is called.

Inside merge_consumer_route, the merge proceeds in two sub-steps:

  1. Consumer-group plugins are written into the merged route; _from_consumer = true is set on each plugin that was not already present in the route.
  2. Consumer plugins are then written, again with _from_consumer = true on new additions, and overwrite any same-named consumer-group plugin.

This produces the three-tier inheritance for consumer groups:

Consumer > Consumer Group > Route (already merged with plugin-config & service)

The _from_consumer flag is checked in _M.filter() to correctly handle the rewrite_in_consumer phase: newly-added consumer plugins run their rewrite logic after route plugins have already run theirs.


run_policy: prefer_route#

Some plugins declare run_policy = "prefer_route". For these, if the plugin exists and is enabled on the route, the version from the service/consumer-group/consumer is skipped during _M.filter(). This lets operators lock a plugin to its route-level configuration regardless of consumer settings.


Caching#

Both merge paths use LRU caches keyed on composite modifiedIndex strings to avoid redundant deep-copies:

  • Service/route merge: keyed on route.id#route.modifiedIndex#service.modifiedIndex
  • Consumer/route merge: keyed on route.id#route.modifiedIndex#consumer.id#consumer.modifiedIndex[#group.id#group.modifiedIndex]
  • Plugin-config merge: uses prev_plugin_config_ver on the route object for a lightweight early-exit check

Key Source Files#

FilePurpose
apisix/plugin.luaCore merge functions: merge_service_route, merge_consumer_route, filter
apisix/plugin_config.luaPlugin-config → route merge and idempotency logic
apisix/consumer_group.luaSyncs /consumer_groups from etcd; provides get(id)
apisix/init.luaOrchestrates all merge calls during http_access_phase