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_routestarts 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:
- Consumer-group plugins are written into the merged route;
_from_consumer = trueis set on each plugin that was not already present in the route. - Consumer plugins are then written, again with
_from_consumer = trueon 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_veron the route object for a lightweight early-exit check
Key Source Files#
| File | Purpose |
|---|---|
apisix/plugin.lua | Core merge functions: merge_service_route, merge_consumer_route, filter |
apisix/plugin_config.lua | Plugin-config → route merge and idempotency logic |
apisix/consumer_group.lua | Syncs /consumer_groups from etcd; provides get(id) |
apisix/init.lua | Orchestrates all merge calls during http_access_phase |