Hi! 👋 We are doing a big documentation refresh. Help us improve — what's missing or could be better? Let us know! Simply send an email or start a conversation in Google Groups!

ProxySQL Plugin Chassis

Availability

The plugin chassis is available in ProxySQL 4.0 builds. It loads native shared libraries during process startup and lets them register Admin tables, Admin commands, runtime projections, metrics, and protocol services.

Plugins execute inside the ProxySQL process with its privileges. They are not sandboxed and cannot be loaded, unloaded, or replaced through the Admin interface. Treat every plugin as trusted native code and plan a ProxySQL restart for any change to the configured plugin list or library.

The operator kill switches --no-plugins and PROXYSQL_NO_PLUGINS=1 bypass all configured plugins for that process start. They are useful for recovering from a plugin that prevents startup without first editing proxysql.cnf.

Loading Plugins

List absolute shared-library paths in the top-level plugins array in proxysql.cnf:

plugins = (
    "/usr/lib64/proxysql/first_plugin.so",
    "/usr/lib64/proxysql/second_plugin.so"
)

The list is read from the configuration file during early startup. It is not an Admin variable and is not persisted in the on-disk ProxySQL database. The file order is the load, initialization, and start order; shutdown runs in reverse order.

For every entry, ProxySQL uses dlopen() with RTLD_NOW | RTLD_LOCAL, then resolves the one required exported symbol:

extern "C" const ProxySQL_PluginDescriptor *proxysql_plugin_descriptor_v1();

RTLD_NOW makes unresolved dependencies a startup error. RTLD_LOCAL keeps symbols defined by the plugin out of the process-wide global namespace; it does not prevent normal resolution of the plugin’s linked dependencies. Plugins should link their declared dependencies and use only core capabilities exposed through the plugin ABI, rather than relying on unsupported ProxySQL executable symbols being globally exported.

Restart ProxySQL after changing the library, its path, or the plugins array. Runtime plugin reload is not supported in 4.0.

Lifecycle

Startup is ordered so a plugin can declare its SQLite schema before the Admin module materializes tables:

  1. Load: ProxySQL opens each library, resolves the descriptor entry point, and validates the descriptor and ABI.
  2. register_schemas (optional, ABI 2+): the plugin registers tables, commands, aliases, and runtime views. Database-handle getters return nullptr in this phase and must not be used. Query-hook registration returns false here.
  3. Admin materialization: ProxySQL merges the registered table definitions into the Admin, disk, and stats schema and creates them. A DDL failure aborts startup.
  4. init: the plugin receives fully initialized services and may complete context setup. ABI 1 plugins and ABI 2+ plugins that omit register_schemas register their tables and commands here.
  5. start: the plugin opens listeners and starts worker threads. ProxySQL calls this only after every plugin has completed init.

The public descriptor is:

struct ProxySQL_PluginDescriptor {
    const char *name;
    uint32_t abi_version;
    bool (*init)(ProxySQL_PluginServices *);
    bool (*start)();
    bool (*stop)();
    const char *(*status_json)();
    bool (*register_schemas)(ProxySQL_PluginServices *); // ABI 2+
};

The descriptor and the string returned by status_json must have static storage duration. Each callback pointer may be null; a null callback opts out of that phase. status_json is reserved in the descriptor, but v4.0.10 does not expose a SHOW PLUGIN STATUS operator command.

Shutdown calls a non-null stop once, in reverse load order, for every plugin whose init phase completed. A null init is an explicit opt-out that counts as completing the phase, so a plugin with null init and non-null stop is still stopped. stop is also called if that plugin’s start fails or a later plugin fails during startup. A plugin whose init callback returned false, or that never reached the init phase, is not stopped. After callbacks finish, ProxySQL closes the shared libraries.

Admin Tables, Commands, and Runtime Views

ProxySQL_PluginServices is the supported bridge from a plugin into ProxySQL. The principal services are:

ServiceReleased behavior
register_tableDeclares a table in admin_db, config_db, or stats_db; the chassis copies the name and DDL. Duplicate names within the same database are rejected.
register_commandRegisters one canonical Admin SQL spelling and its callback.
register_command_aliasMaps alternate spellings to an already registered canonical command.
register_runtime_viewRegisters an on-demand projection callback for an Admin or stats table.
get_admindb, get_configdb, get_statsdbReturn live SQLite handles from init onward; return nullptr during register_schemas.
log_messageSends level 3 as error, level 4 as warning, and other levels as info.
get_prometheus_registryReturns the shared Prometheus registry so plugin metrics use the normal metrics endpoint.

An Admin command callback receives borrowed Admin, config, and stats database handles plus the canonical SQL. It returns an error code, affected-row count, and message. Error code 0 produces an OK packet; a nonzero code produces an error packet.

Command matching is case-insensitive. ProxySQL trims leading and trailing whitespace and a trailing semicolon, and collapses internal whitespace. Alias collisions and duplicate canonical registrations fail the plugin registration phase instead of silently shadowing another command.

Use the normal three-layer model for plugin configuration:

DISK (config_db) <-> MEMORY (editable admin_db table) <-> RUNTIME (plugin-owned state)

A runtime_<name> table is a read-only Admin projection, not the plugin’s runtime storage. LOAD <name> TO RUNTIME reads the editable table and atomically installs state in the plugin module. SAVE <name> TO MEMORY (or a registered FROM RUNTIME alias) dumps module state into the editable table. Before an Admin SELECT references a registered runtime table as a complete identifier, the chassis invokes its projection callback to rebuild that table from module state.

ABI 4 adds db_kind to ProxySQL_PluginRuntimeView, so a projection can receive the matching Admin, config, or stats database handle. Disk-to-memory and memory-to-disk commands remain transactional SQLite copies; an empty source must still clear the destination.

ABI Compatibility

The v4.0.10 header defines both PROXYSQL_PLUGIN_ABI_VERSION and PROXYSQL_PLUGIN_ABI_VERSION_MAX as 4. Its loader accepts descriptor ABI versions 1, 2, 3, and 4 and rejects 0 or any version above 4.

ABICompatible surface
1Six-field descriptor: name, version, init, start, stop, and status_json. It has no register_schemas field.
2Appends register_schemas to the descriptor and adds the extended services for query hooks, Prometheus, and command aliases.
3Keeps the ABI 2 descriptor layout and appends register_runtime_view to ProxySQL_PluginServices.
4Keeps the ABI 3 descriptor and services layouts and appends db_kind to ProxySQL_PluginRuntimeView. A three-field ABI 3 aggregate defaults the new field to admin_db (0).

New fields are tail-appended, and the loader gates reads by the plugin’s declared version. That permits an older ABI plugin to run on the v4.0.10 chassis. A plugin built for a newer ABI is rejected rather than letting the old core read an incompatible layout.

This is a C++ ABI, not a pure C ABI. ProxySQL_PluginCommandResult and query-hook results contain std::string, and the services surface includes prometheus::Registry. Build plugins with the same C++17 compiler, standard library ABI, prometheus-cpp headers, and ProxySQL feature-tier definitions as the target core. The safest supported workflow is to build inside the matching ProxySQL source tree and set abi_version from PROXYSQL_PLUGIN_ABI_VERSION, never a hard-coded number.

Use hidden symbol visibility and explicitly export only proxysql_plugin_descriptor_v1. Do not link a plugin against ProxySQL internal static libraries; use the services callbacks at the ABI boundary.

Failure and Shutdown Behavior

ProxySQL treats a configured plugin as required. Any of these failures abort startup:

  • duplicate plugin path;
  • dlopen() failure, including a missing dependency or wrong architecture;
  • missing proxysql_plugin_descriptor_v1 symbol or a null descriptor;
  • null or empty descriptor name;
  • ABI outside the accepted 1 through 4 range;
  • duplicate or invalid table, command, alias, or runtime-view registration;
  • register_schemas, schema materialization, init, or start failure.

If register_schemas or init returns false, or a registration reports failure, startup aborts. Before returning failure, v4.0.10 discards the Admin, config, and stats table definitions, command and alias entries, and copied table-name/DDL storage added during that callback. This rollback does not cover runtime-view or query-hook registrations. On a start failure, ProxySQL runs the normal stop path for every plugin whose init phase completed. A stop callback failure is logged, does not cause a second stop attempt, and does not prevent the remaining libraries from being closed.

Security and File Permissions

A plugin can read and modify ProxySQL’s in-process state through its callbacks and can execute arbitrary native code. Apply the same trust review used for the ProxySQL binary itself.

  • Keep libraries in a directory that is not writable by the ProxySQL service account or unprivileged users.
  • Make the library and every parent directory readable/traversable by the account that starts ProxySQL.
  • Prefer absolute, versioned paths and an atomic symlink switch performed by a privileged deployment process.
  • Verify ownership and hashes before restart. Do not load a shared library from a temporary, home, or build directory in production.
  • Remember that the plugins array is security-sensitive configuration even though it is not stored in the Admin database.

Troubleshooting

Start with the ProxySQL error log; loader failures include the failing phase and the dynamic-loader detail when available.

# Confirm the configured file and its permissions.
namei -l /usr/lib64/proxysql/my_plugin.so
file /usr/lib64/proxysql/my_plugin.so

# Find unresolved shared-library dependencies.
ldd /usr/lib64/proxysql/my_plugin.so

# Confirm that exactly the required descriptor symbol is exported.
nm -D --defined-only /usr/lib64/proxysql/my_plugin.so \
  | grep proxysql_plugin_descriptor_v1

Common diagnostics:

  • dlopen failed: verify the path, architecture, permissions, and every dependency reported by ldd.
  • missing descriptor symbol: export the entry point with C linkage and default visibility.
  • unsupported ABI version: rebuild against the v4.0.10 header or install a core that supports the plugin’s ABI.
  • registration failed: check for a duplicate table, command, alias, null callback, or malformed DDL.
  • schema phase sees null DB handles: move database access to init; register_schemas is declaration-only.
  • startup loop after deploying a plugin: start once with --no-plugins or PROXYSQL_NO_PLUGINS=1, correct the library or configuration, and restart normally.

Public API Scope

For v4.0.10, the public plugin-author contract is the released include/ProxySQL_Plugin.h descriptor, data types, callbacks, and services described above. The following boundaries are intentional:

  • get_mysql_users_snapshot, get_mysql_servers_snapshot, and get_mysql_group_replication_hostgroups_snapshot are present in the services struct but return nullptr in every released phase.
  • Query-hook registration supports one configured callback per protocol. Eligible v4.0.10 MySQL and PostgreSQL traffic dispatches that callback when one is configured; a deny result returns a protocol-appropriate error to the client and the query is not routed to a backend. Treat a hook as in-process enforcement code: fail-open behavior inside a plugin, including its own disabled or uninitialized state, remains the plugin’s responsibility.
  • status_json has no released Admin command that displays it.
  • Internal loader-manager helpers and generated or test-only build artifacts are implementation machinery, not supported plugin APIs.

Code against the released header and documented lifecycle rather than copying internal loader classes or generated files.