PostgreSQL Extensions — Replica Crash When .so Missing
After installing pg_stat_statements on primary, replica crashed with 'could not load library' error and infinite lag.
20+ years shipping high-throughput database systems. Everything here is grounded in real deployments.
- ✓Deep production experience
- ✓Understanding of internals and trade-offs
- ✓Experience debugging complex systems
- Extensions are bolt-on modules that inject new types, functions, or index methods into PostgreSQL
- CREATE EXTENSION reads a .control file, executes an SQL script, and registers objects in pg_extension
- shared_preload_libraries is required for some extensions — missing it causes silent failure
- HNSW indexes in pgvector cost 2-3x more disk space than IVFFlat but give ~5% better recall
- Production trap: upgrading the OS package without running ALTER EXTENSION UPDATE leaves the DB version stale
Think of PostgreSQL like a smartphone. It comes with a camera, phone app, and messages out of the box — but you can install apps to do way more. PostgreSQL extensions are those apps: bolt-on features that live inside the database itself, like GPS navigation or a video editor. You choose exactly which 'apps' your database needs. No bloat, no rewrites — just enable what you want and it's ready to use.
PostgreSQL ships as one of the most capable relational databases on the planet, but its real competitive edge isn't what it does by default — it's what it can become. Extensions let PostgreSQL morph into a time-series engine, a geospatial powerhouse, a vector similarity search system, or a statistical analysis platform without ever leaving SQL. This isn't a niche feature — it's the architectural choice that lets a single Postgres cluster replace entire categories of specialised databases.
The problem extensions solve is elegant: database internals are hard to change safely at runtime, but application requirements change constantly. Before extensions, adding new data types, indexing strategies, or procedural languages meant patching the core and recompiling. Extensions formalise a safe, versioned, reversible mechanism for injecting new capabilities into a live cluster — complete with dependency tracking, upgrade paths, and schema isolation. They're the reason PostGIS can add full geographic primitives and pgvector can power AI embedding search inside the same Postgres instance your users' accounts live in.
By the end of this article you'll know exactly how extensions work under the hood — from the shared object loading mechanism to the extension control file format. You'll be able to install and audit extensions safely in production, understand the performance implications of popular extensions, and build a minimal custom extension from scratch. No fluff, just the internals and the gotchas that bite in production.
Why PostgreSQL Extensions Are Shared Libraries That Must Be Present on Every Replica
PostgreSQL extensions are dynamically loaded shared libraries (.so files) that hook into the database's internal APIs to add new data types, index methods, or procedural languages. Unlike application-level plugins, these libraries execute inside the database process itself — meaning a missing .so on a replica doesn't just break a query; it crashes the entire backend. The core mechanic is simple: CREATE EXTENSION records the library name in the system catalog, and at runtime, PostgreSQL uses dlopen() to load it. If the file isn't on disk at the expected path, the process segfaults.
In practice, extensions like PostGIS or pg_cron ship as .so files that must be installed identically on every node in a replication cluster. The extension's SQL objects (functions, operators, types) are replicated via WAL, but the underlying shared library is not. This asymmetry is the root cause of replica crashes: the SQL references a function that requires the library, but the library isn't there. The failure is immediate and unrecoverable — the replica process dies, and replication stalls until manual intervention.
Use extensions when you need deep integration with PostgreSQL internals — custom index access methods, foreign data wrappers, or specialized data types. Avoid them for simple application logic that could live in a schema or a separate service. In production, every extension you add becomes a deployment dependency: the .so must be present on all nodes before the CREATE EXTENSION runs. This is non-negotiable.
How PostgreSQL Extensions Actually Work Internally
Every extension is made of three things: a shared object file (.so on Linux, .dll on Windows), a SQL script that defines objects in the database, and a control file that ties it all together. When you run CREATE EXTENSION, Postgres reads the control file from $sharedir/extension/, executes the install SQL script, and registers every object the extension created in the pg_extension and pg_depend system catalogs. That catalog registration is the secret sauce — it means Postgres knows which tables, functions, operators, and types belong to the extension, so DROP EXTENSION CASCADE can clean up everything safely.
The shared object is loaded into the backend process on first use via dlopen(). This means extension code runs in the same memory space as PostgreSQL itself. A poorly written C extension can segfault the entire backend — there's no sandbox. That's why extensions from trusted sources and your Linux package manager (postgresql-16-postgis-3, for example) are fundamentally safer than compiling random GitHub repos in production.
Extensions live in a specific schema (default: public, but you can redirect with the schema parameter). The search_path matters enormously here — if the extension's schema isn't on your search_path, function calls will fail with 'function not found' even though the extension is installed. Always check \dx in psql and pg_extension in SQL to see exactly what's active and which schema it landed in.
Installing and Managing Extensions in Production — The Right Way
Installing an extension is one line of SQL, but doing it safely in production involves four distinct steps that most tutorials skip entirely.
First, the shared library must exist on the filesystem of every PostgreSQL server in your cluster — including replicas, because replay of CREATE EXTENSION on a standby will fail if the .so file isn't present. This means your deployment pipeline must install the OS package before the SQL runs, not after.
Second, some extensions require preloading into shared memory at startup via shared_preload_libraries in postgresql.conf. pg_stat_statements is the classic example — if it's not in that list, CREATE EXTENSION succeeds but all the views return zero rows and no error is raised. Silent failure at its most frustrating.
Third, only superusers can CREATE EXTENSION by default. In managed cloud environments (RDS, Cloud SQL, AlloyDB) you get a pseudo-superuser role like rds_superuser that can install from a pre-approved allowlist. You cannot install arbitrary extensions on managed Postgres — this is a deliberate security boundary.
Fourth, extension upgrades are separate from OS package upgrades. Updating the debian package gets you new .so and SQL files on disk, but the database still runs the old version until you explicitly run ALTER EXTENSION name UPDATE. Both steps are required and order matters: package first, ALTER EXTENSION second.
Three Extensions You Should Know Deeply — pgvector, PostGIS, and pg_partman
Knowing how to run CREATE EXTENSION is table stakes. Knowing the performance model and operational nuances of specific extensions is what separates a database engineer from someone who just read the docs.
pgvector adds vector data types and approximate nearest-neighbour search, making Postgres a viable store for AI embedding search. Its HNSW index (added in 0.5.0) dramatically outperforms the older IVFFlat index for most workloads, but HNSW builds are memory-intensive — each connection building the index uses maintenance_work_mem, and building in parallel multiplies that.
PostGIS is the gold standard for geospatial work, but it adds two extension layers: postgis (core) and optionally postgis_topology and postgis_raster. The ST_DWithin function with a geography (not geometry) column correctly handles great-circle distance but is ~10x slower than geometry unless you have a spatial index on the column.
pg_partman automates partition maintenance — creating future partitions and dropping old ones on a schedule. It runs as background worker processes and requires pg_cron or a similar scheduler. The critical gotcha: pg_partman won't automatically attach data inserted into the parent table to the correct child partition unless you also configure partition_data correctly. Orphaned rows in the parent table silently kill query performance.
Building a Custom PostgreSQL Extension from Scratch
Building your own extension demystifies every extension you'll ever use and opens the door to organisation-specific functionality you can version, test, and deploy just like application code.
A minimal extension needs exactly three files: a control file (name.control), a SQL installation script (name--version.sql), and optionally a C file compiled to a shared object for performance-critical or type-level functionality. Pure SQL extensions are fully portable and need no compilation — they're underused and underappreciated.
The control file specifies the extension's identity: its default version, whether it's relocatable, what schema it prefers, and which other extensions it depends on. The requires field is how Postgres enforces extension dependency ordering — if your extension depends on uuid-ossp, Postgres will refuse to install yours without it.
The SQL script runs with the privileges of the user calling CREATE EXTENSION, inside a transaction. If any statement fails, the whole installation rolls back — a beautiful guarantee. Objects created in the script are automatically tagged as extension-owned in pg_depend, so you don't need to manage that yourself.
For production custom extensions, store them in a git repo, use PGXS (the extension build system that ships with PostgreSQL) to compile and install, and write pgTAP tests against your install/upgrade/uninstall scripts before they touch any real cluster.
Extension Security and Permission Models — The Defensive Side
Extensions run inside the database backend — no sandbox, no isolation. That means a malicious or buggy extension can read any data the backend can access, including memory of other connections. PostgreSQL's trust model relies on the superuser installing extensions, but after installation, the objects are owned by the superuser by default. However, any user can call extension functions if they have EXECUTE privilege, unless you revoke it.
For extensions like pg_stat_statements that expose performance data, the reset function is dangerous — any user can wipe hours of monitoring data if you don't revoke PUBLIC access. For extensions that create new data types (like pgvector or PostGIS), columns of those types can be accessed by any user if the table's permissions allow.
The safest pattern: install non-relocatable extensions in a dedicated schema, revoke all privileges from PUBLIC on extension functions, and grant only to specific roles. For relocatable extensions, use the SCHEMA option to isolate them. Always audit with \dp+ and check function permissions.
Also consider that extensions can introduce background workers (like pg_partman). These workers run with the privileges of the user who configured them — if that user is a superuser, the worker has full access. Limit background worker privileges to only what's needed.
- Any extension function you grant to PUBLIC can be called by any database user — no sandboxing.
- Background workers (pg_partman, pg_cron) run as the role that configured them — if that's a superuser, they have full access.
- Revoke EXECUTE on dangerous functions like
reset()immediately after CREATE EXTENSION. - Install extensions in dedicated schemas to namespace their objects and control search_path access.
- Audit extension object permissions regularly with queries against pg_depend and pg_proc.
What Extensions Actually Are (And Why Most Devs Get Them Wrong)
Extensions aren't plugins. They're shared objects — compiled C libraries that get linked into the backend process at runtime. When you run CREATE EXTENSION, you're not installing software. You're registering a set of SQL objects (functions, operators, data types, casts) that call into that loaded library.
That distinction matters because it explains every painful production failure you'll ever see with extensions. The extension exists in two places: the filesystem (the .so file) and the database catalog (the registered objects). If those get out of sync — say you pg_dump from a system with PostGIS 3.0 and restore to one with 3.4 — your restore silently succeeds until someone queries a geometry column and the backend hard-crashes.
The catalog entry stores a version string, not a binary hash. PostgreSQL trusts that you've deployed the matching library. It does not verify. This is why extension upgrades must be explicit, never implicit during restores.
When You Should (And Shouldn't) Use an Extension
Extensions solve real problems: adding vector search, geospatial types, partitioning orchestration, or full-text parsing improvements. But every extension you add is a deployment dependency and a potential crash vector. The C library runs in the same process as your backend. A segfault in pgvector takes down every connection on that server. No isolation.
Use extensions when: the feature is fundamental to your data model (PostGIS for location data), it saves you from writing and maintaining thousands of lines of procedural code (pg_partman), or it gives you a capability that would require a separate service (pgvector for embeddings).
Don't use extensions for: minor convenience, one-off analytics queries you can write in SQL, or anything with a CVE history you haven't vetted. Every extension is a supply chain risk. You're loading unsigned code into your database process. Check pgxn.org and the extension's GitHub issues before deploying. If the repo has 3 stars and the author hasn't pushed in 2 years, walk away.
And never install an extension "just to see what it does" on production. Development or a dedicated test instance only. I've watched a junior run CREATE EXTENSION on a prod replica and trigger a failover because the extension tried to allocate shared memory that didn't exist on the standby.
pg_stat_statements: The One Extension You Install Before You Debug Anything
Stop guessing why your database is slow. pg_stat_statements is the only extension that gives you the actual numbers — query frequency, latency, I/O, temp file usage, and blocking time. It's a shared library that hooks into the executor to track every query's runtime statistics. Without it, you're flying blind.
Install it. Enable it. Query pg_stat_statements sorted by total_time descending. That's your hit list. Every normalization failure, every sequential scan on a million-row table, every forgetten index — this extension exposes them. Production postmortems start here. The catch? It must be loaded at cluster startup via shared_preload_libraries, not on the fly. Plan your restarts accordingly. Once it's in, you get zero-cost visibility into query performance. Every replica needs it too — same shared object, same version, or queries crash.
pg_stat_statements_reset() wipes all history. Do this during maintenance windows only — you lose trend data for capacity planning.hstore: The Key-Value Extension You Never Knew You Needed (Until You Don't)
hstore stores arbitrary key-value pairs in a single column. No schema changes. No null columns piling up. It's a hash map inside PostgreSQL — => syntax, ? for existence checks, #> for path access. Performance is solid because it's backed by C-level hash tables.
Why use it? When you need flexible attributes that don't justify a full EAV table or a JSONB column. Product metadata, A/B test flags, user preferences — hstore handles them with less overhead than JSONB for simple string values. No parsing cost, no nested object complexity. The killer feature? GIN indexes on ? and @> operators make lookups fast even across millions of rows. But do not store values over 1KB here — you'll bloat indexes. And forget about nested data; hstore is flat by design. That's the trade-off: raw speed for limited structure. Use it where JSONB is overkill.
Sharp H2
PostgreSQL extensions are not magic—they are shared objects loaded into the backend process, but their real power comes from the design choices you make. The three pillars we've explored—pgvector for AI embeddings, PostGIS for spatial data, pg_partman for time-series partitioning—demonstrate how extensions can solve specific problems without bloating your database. But the lesson isn't just about these tools. It's about understanding when an extension is a shortcut to production stability, not a toy. The defensive model we built around security—stricter search_path, explicit schema grants, and revoking dangerous functions—protects you from the very power extensions provide. Extensions should always be deployed via idempotent migration scripts, not ad-hoc SQL. They must be version-locked across replicas, tested in staging, and never left to drift. The future of PostgreSQL is an ecosystem of purpose-built extensions, but only disciplined teams will harness them safely. Remember: an extension is a contract with your database, not a temporary fix. Treat it like one.
Sharp H2
Before you ship that next extension, ask yourself: does the problem genuinely require a database-native solution, or is it a caching layer, an external service, or a simpler SQL construct in disguise? The most common production failures come from extensions that do too much—bloating shared_buffers, locking catalog tables during CREATE EXTENSION, or silently consuming disk with TOAST tables. We've seen teams install pg_partman without setting retention policies, resulting in 2TB partition tables that were never cleaned up. The rule of thumb: use extensions for what they uniquely provide—indexing algorithms (pgvector's IVFFlat), spatial types (PostGIS's GEOMETRY), or automated partitioning (pg_partman's time-based triggers). For everything else—rate limiting, complex business logic, or HTTP calls—use application code. Extensions add surface area: each one is a potential vulnerability (CVE history), a performance overhead during writes, and a migration blocker when upgrading PostgreSQL major versions. The disciplined path is to audit your current extensions quarterly, remove unused ones, and always test upgrades in a pre-prod environment with production-like data volumes.
PostgreSQL Extension Management: CREATE EXTENSION and Trusted Extensions
Managing extensions in PostgreSQL involves more than just running CREATE EXTENSION. Understanding the difference between trusted and untrusted extensions is crucial for production environments. Trusted extensions are those that can be installed by any user with the CREATE privilege on the database, while untrusted extensions require superuser privileges. This distinction is enforced by the extension's control file, which includes a trusted flag. For example, pg_stat_statements is trusted, while postgis is not. To install a trusted extension, a non-superuser can run:
``sql CREATE EXTENSION IF NOT EXISTS pg_stat_statements; ``
For untrusted extensions, superuser privileges are required:
``sql -- Must be run by superuser CREATE EXTENSION postgis; ``
Additionally, the CREATE EXTENSION command can specify a version, schema, and whether to cascade dependencies. For instance:
``sql CREATE EXTENSION postgis VERSION '3.0.0' SCHEMA public CASCADE; ``
When upgrading extensions, use ALTER EXTENSION ... UPDATE:
``sql ALTER EXTENSION postgis UPDATE TO '3.1.0'; ``
In production, always test extension upgrades in a staging environment first. Also, be aware that some extensions may require additional shared library files (.so) on every replica, as discussed in the article. Proper management ensures consistency across the cluster.
ALTER EXTENSION ... UPDATE for upgrades, and ensure the extension's shared library is present on all replicas.CREATE EXTENSION with version and schema control, and understand the trusted/untrusted distinction to maintain security and consistency.pgvector: Vector Similarity Search in PostgreSQL
pgvector is a PostgreSQL extension that enables efficient vector similarity search, essential for AI and machine learning applications like semantic search, recommendation systems, and anomaly detection. It introduces a new data type vector and supports exact and approximate nearest neighbor search using indexes like IVFFlat and HNSW. To get started, install the extension and create a table with a vector column:
```sql CREATE EXTENSION vector;
CREATE TABLE items ( id bigserial PRIMARY KEY, embedding vector(1536) -- 1536 dimensions for OpenAI embeddings ); ```
Insert data by converting arrays to vectors:
``sql INSERT INTO items (embedding) VALUES ('[0.1, 0.2, ...]'::vector); ``
Query for the top 5 most similar items using cosine distance:
``sql SELECT * FROM items ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector LIMIT 5; ``
For performance, create an index. IVFFlat is faster to build but less accurate, while HNSW is more accurate but slower to build:
```sql -- IVFFlat index with 100 lists CREATE INDEX ON items USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
-- HNSW index (pgvector >= 0.5.0) CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops); ```
In production, tune index parameters based on data size and query latency requirements. pgvector is a game-changer for bringing vector search into PostgreSQL without external services.
PostGIS, pg_partman, pg_cron: Production Extension Stack
In production PostgreSQL environments, a combination of extensions can address common challenges: spatial data management (PostGIS), automated partitioning (pg_partman), and job scheduling (pg_cron). These extensions work together to build scalable, maintainable systems. PostGIS adds support for geographic objects, allowing spatial queries like distance calculations and bounding box searches. pg_partman automates table partitioning, crucial for time-series data. pg_cron enables cron-like scheduling within PostgreSQL, useful for maintenance tasks.
Example: A time-series table with spatial data can be partitioned by day using pg_partman, and a pg_cron job can run nightly to create new partitions:
```sql -- Create partitioned table CREATE TABLE sensor_data ( id bigserial, recorded_at timestamptz NOT NULL, location geometry(Point, 4326), value float8 ) PARTITION BY RANGE (recorded_at);
-- Set up pg_partman SELECT partman.create_parent( p_parent_table := 'public.sensor_data', p_control := 'recorded_at', p_type := 'native', p_interval := '1 day', p_premake := 30 );
-- Schedule partition maintenance with pg_cron SELECT cron.schedule('create-partitions', '0 0 *', $$CALL partman.run_maintenance()$$);
-- Spatial query: find sensors within 1km of a point SELECT id, value FROM sensor_data WHERE ST_DWithin(location, ST_SetSRID(ST_MakePoint(-73.935242, 40.730610), 4326), 1000); ```
This stack is battle-tested in production at scale. PostGIS requires careful indexing (GIST indexes on geometry columns), pg_partman needs regular maintenance calls, and pg_cron should be monitored for job failures. Together, they provide a robust foundation for geospatial time-series applications.
EXPLAIN ANALYZE to verify index usage. Consider using pg_partman's background worker for automatic maintenance instead of pg_cron for lower latency.Replica Crash After Installing pg_stat_statements
dlopen(). If that file doesn't exist on the standby's filesystem, the standby backend crashes and the WAL replay stops. The team had only installed the postgresql-16-pg-stat-statements package on the primary.- Always install extension OS packages on all cluster nodes before running CREATE EXTENSION on the primary.
- Treat the .so file deployment as a prerequisite in your runbook — not an afterthought.
- After installing a new extension, verify replication health with SELECT
pg_is_in_recovery()and check pg_stat_replication for lag.
SELECT name, setting, pending_restart FROM pg_settings WHERE name = 'shared_preload_libraries';If missing: ALTER SYSTEM SET shared_preload_libraries = 'pg_stat_statements'; then restart cluster.| File | Command / Code | Purpose |
|---|---|---|
| inspect_extensions.sql | SELECT | How PostgreSQL Extensions Actually Work Internally |
| production_extension_lifecycle.sql | ALTER SYSTEM SET shared_preload_libraries = 'pg_stat_statements, pg_prewarm'; | Installing and Managing Extensions in Production |
| pgvector_hnsw_production.sql | CREATE EXTENSION IF NOT EXISTS vector; -- installs the vector type and operator... | Three Extensions You Should Know Deeply |
| build_custom_extension.sh | set -euo pipefail | Building a Custom PostgreSQL Extension from Scratch |
| extension_security_audit.sql | SELECT | Extension Security and Permission Models |
| ExtensionCatalogCheck.sql | SELECT e.extname, | What Extensions Actually Are (And Why Most Devs Get Them Wro |
| ExtensionRiskAudit.sql | SELECT e.extname, | When You Should (And Shouldn't) Use an Extension |
| TopQueries.sql | SELECT | pg_stat_statements |
| ProductAttributes.sql | CREATE EXTENSION IF NOT EXISTS hstore; | hstore |
| verify_extensions.sql | SELECT e.extname, e.extversion, | Sharp H2 |
| audit_extensions.sql | SELECT e.extname, e.extversion, | Sharp H2 |
| extension-management.sql | CREATE EXTENSION IF NOT EXISTS pg_stat_statements; | PostgreSQL Extension Management |
| pgvector-example.sql | CREATE EXTENSION vector; | pgvector |
| production-stack.sql | CREATE EXTENSION postgis; | PostGIS, pg_partman, pg_cron |
Key takeaways
Interview Questions on This Topic
What's the difference between CREATE EXTENSION and simply running the extension's SQL script manually — and why does it matter for schema management?
Frequently Asked Questions
20+ years shipping high-throughput database systems. Everything here is grounded in real deployments.
That's MySQL & PostgreSQL. Mark it forged?
12 min read · try the examples if you haven't