Serverless Functions or Custom Plugins
API7 Gateway gives you two ways to run your own Lua logic in the request lifecycle: the built-in serverless-function plugins and custom Lua plugins. Both execute Lua you write, with the same access to the gateway runtime. They differ in how the code is packaged, configured, governed, and reused.
As a rule of thumb: use serverless functions for small, one-off snippets, and use a custom plugin for anything reusable or complex.
Serverless Functions
The serverless-pre-function and serverless-post-function plugins run inline Lua that you paste directly into a route, service, consumer, or global rule. There is nothing to upload or register — the code is a field of the resource configuration. See the Serverless Functions plugin reference for the full configuration.
Each instance runs a list of functions in a single phase. Every function is a Lua string that returns a function. The returned function receives the plugin config and request context, so its full signature is function(conf, ctx), but it can also ignore them and take no arguments:
{
"plugins": {
"serverless-pre-function": {
"phase": "access",
"functions": [
"return function(conf, ctx) local core = require('apisix.core'); core.request.set_header(ctx, 'X-Trace-ID', 'demo') end"
]
}
}
}
phase— the single phase to run in:rewrite,access,header_filter,body_filter,log, orbefore_proxy. Defaultaccess.functions— an array of Lua source strings, each returning afunction(conf, ctx). They run in order; if one returns a status code or body, the request short-circuits.
serverless-pre-function runs early (priority 10000) and serverless-post-function runs late (priority -2000), which is why they are typically used to inject logic at the very start or very end of a phase.
Custom Plugins
A custom plugin is a Lua module you upload once from the Dashboard. The control plane parses its name, version, and schema, distributes it to the gateway groups you select, and then it appears in the plugin picker like any built-in plugin. You configure it per route or service through typed fields, not by pasting code.
local core = require("apisix.core")
local schema = {
type = "object",
properties = {
header_name = { type = "string", default = "X-Trace-ID" },
header_value = { type = "string" },
},
required = { "header_value" },
}
local _M = {
version = 0.1,
priority = 1500,
name = "custom-header",
schema = schema,
}
function _M.check_schema(conf)
return core.schema.check(schema, conf)
end
function _M.access(conf, ctx)
core.request.set_header(ctx, conf.header_name, conf.header_value)
end
return _M
Comparison
| Dimension | Serverless Functions | Custom Plugins |
|---|---|---|
| Where the code lives | Inline, inside each resource's configuration | A named module uploaded to the control plane |
| Reuse | Copy and paste into every resource that needs it | Registered once, referenced by name in any deployed gateway group |
| Configuration | None — parameters are hard-coded in the Lua string | Typed fields validated by a JSON schema with defaults |
| Name and version | None | Parsed and enforced (name, version) |
| Priority | Fixed: 10000 (early) or -2000 (late) | Set by the plugin (priority field) |
| Phases | One phase per instance | Any or all six phases in one module |
| Rollout control | Follows the resource it is attached to | Deployed to selected gateway groups; promote from test to production |
| Review and governance | Reviewed per resource, wherever it is pasted | Uploaded and versioned centrally, governed by RBAC |
| Testing | Hard — logic is an escaped string in config | Testable as a module (luac -p, route tests) |
| Runtime access | Full gateway runtime | Full gateway runtime |
| Best fit | Small, one-off snippets and quick experiments | Reusable, complex, production logic |
Both mechanisms run with the same privileges — neither is sandboxed. Review the code with the same care regardless of which you choose.
When to Use Serverless Functions
Serverless functions are a good fit when the logic is:
- Small and self-contained — a few lines to set a header, tweak a variable, or emit a log line.
- Specific to one resource — you do not expect to reuse it elsewhere.
- A quick experiment — you want to try something without the upload-and-register cycle.
When to Use Custom Plugins
Choose a custom plugin when the logic is anything more than trivial. In particular, use one when you need:
- Configuration. A typed, validated
schemawith defaults, so operators configure the plugin through fields instead of editing Lua. This is the biggest practical difference. - Reuse. One named plugin applied across many routes and services, updated in one place.
- Controlled rollout. Deploy to a test gateway group, validate, then add the production group. See Roll Out Across Gateway Groups.
- Multiple phases or a specific priority. Logic that spans, for example, both
accessandlog, or that must run at a particular point relative to other plugins. - Dependencies. Shared helper modules or native libraries on the package path. See Add Dependency Libraries.
- Review and lifecycle management. Central upload, versioning, and RBAC rather than code scattered across resource configurations.
For complex scenarios, custom plugins are the recommended approach. They keep the logic reusable, testable, configurable, and governed, while serverless functions keep small snippets close to the resource they act on.
Next Steps
- Develop Custom Lua Plugins — build, register, and test a custom plugin.
- Plugin Development Best Practices — write plugin code that is correct and fast.
- Serverless Functions — the serverless-function plugin reference.