dynamic_library_path tells PostgreSQL where to look for a loadable C module when something — a CREATE FUNCTION ... LANGUAGE C, a LOAD, a shared_preload_libraries entry — names the library by a bare filename with no directory in it. The default is $libdir, the context is superuser, and for most of its life this was a parameter nobody touched. PostgreSQL 18 gave it a reason to matter again, which is where we’ll end up.
How the search works
The rule turns on whether the named library has a slash in it. If it does — an absolute path, or one with a directory component — PostgreSQL uses it verbatim and dynamic_library_path is never consulted. If it doesn’t, PostgreSQL walks the path, directory by directory, looking for the file, appending the platform’s shared-library extension if you left it off. So LOAD 'auto_explain' triggers a search; LOAD '/opt/pg/lib/auto_explain' does not.
The path is a colon-separated list of absolute directories (semicolons on Windows), and one token in it is magic: $libdir expands to the compiled-in package library directory, the place the standard distribution installs its modules — what pg_config --pkglibdir prints. The default is just that token alone, which is why a stock cluster finds its extensions without anyone configuring anything. To add a location you include $libdir explicitly alongside yours, because unlike a shell PATH there’s no implicit inheritance — dynamic_library_path = '/usr/local/lib/postgresql:$libdir' searches your directory first, then the standard one. Set it to the empty string and automatic path search is off entirely. (This shape — explicit $libdir, no PATH=$PATH: convenience — is exactly what Peter Eisentraut and Tom Lane settled on when the parameter was designed in 2001, so that a dumped CREATE FUNCTION would still reload against an installation-relative location.)
Why it’s superuser, and mostly moot
Loading a C library executes code inside the backend, which runs as the PostgreSQL operating-system user. That is about as privileged as it gets, so the parameter is superuser-only, and even then a non-superuser’s LOAD is confined to $libdir/plugins/. You can change it at runtime, but only for the current connection — the docs flag that as a development convenience and steer you to postgresql.conf for anything real.
For most installations it’s moot regardless, because the packaging already does the right thing. Build an extension with PGXS, or install one from a distribution package, and the .so lands in $libdir where the default path already looks. You never see the parameter. And on managed PostgreSQL — RDS, Cloud SQL, and friends — you can’t load arbitrary C libraries at all, so the parameter is both unsettable and pointless there.
The PostgreSQL 18 reason to care
The parameter became interesting again because of a long-standing asymmetry that PostgreSQL 18 finally fixed. Since 8.1, dynamic_library_path could relocate a module’s shared library — but when extensions arrived in 9.1, no matching search path was added for their control files and SQL scripts, which could live in exactly one place: the package share/extension directory. The practical result was that extensions weren’t relocatable: you could move the .so but not the .control, so it didn’t matter. PostgreSQL 18 adds extension_control_path to close the gap, and the two are meant to be set together. The docs give the pairing directly:
1 extension_control_path = '/usr/local/share/postgresql:$system'
2 dynamic_library_path = '/usr/local/lib/postgresql:$libdir'
With both pointed at a parallel tree, you can finally install and run extensions entirely outside the system directories — useful when you can’t write to the package paths, when you’re testing an extension build without disturbing the installed copy, or when you want locally-built modules quarantined from the distribution’s. That’s the live use case in 2026: not the old “I hand-built one module” story, but proper relocatable extension installs, with dynamic_library_path as the half that handles the libraries and extension_control_path as the half that was missing for fourteen years.
Leave it at $libdir unless you’re deliberately relocating extensions, and if you are, set its new partner alongside it — one without the other just means PostgreSQL finds half of each extension.