Compatibility
PgCache is designed to be transparent to client applications. This page covers supported PostgreSQL versions, query patterns, client libraries, and known limitations.
PostgreSQL Version Support
| PostgreSQL Version | Status |
|---|---|
| 18 | Supported (primary target) |
| 17 | Supported |
| 16 | Supported |
| 15 and below | Not supported (requires logical replication protocol version 4) |
The origin database must have wal_level = logical enabled for CDC replication.
Note: On PostgreSQL 18+, search_path changes are observed via the native ParameterStatus notifications. On PostgreSQL 16 and 17, pgcache piggybacks SHOW search_path onto detected mutations (SET/RESET search_path, DISCARD ALL, transaction boundaries) to track the current value. While the new value is being resolved, cacheable queries on the affected connection are forwarded to the origin.
Limitation: User-defined functions can change search_path internally (for example, a PL/pgSQL function calling SET LOCAL search_path or set_config()). These changes are not visible to the proxy and will not be reflected in pgcache’s table resolution. If your application relies on functions that mutate search_path, set it explicitly at the session level instead.
Supported Query Patterns
PgCache caches queries that it can safely evaluate and invalidate. All other queries are transparently forwarded to the origin.
Cached
| Pattern | Example |
|---|---|
| Single table SELECT | SELECT * FROM users WHERE active = true |
| INNER JOIN | SELECT u.id, u.name, o.total FROM users u INNER JOIN orders o ON u.id = o.user_id |
| LEFT / RIGHT JOIN | SELECT u.name, o.total FROM users u LEFT JOIN orders o ON u.id = o.user_id |
| USING / NATURAL JOIN | SELECT * FROM users JOIN orders USING (user_id), SELECT * FROM users NATURAL JOIN orders |
| CROSS JOIN | SELECT * FROM colors CROSS JOIN sizes |
| Equality comparisons | WHERE status = 'active' |
| Inequality comparisons | WHERE age > 18, WHERE price <= 100 |
| AND / OR conditions | WHERE active = true AND role = 'admin' |
| IN / NOT IN | WHERE id IN (1, 2, 3) |
| IS NULL / IS NOT NULL | WHERE deleted_at IS NULL |
| NOT expressions | WHERE NOT archived |
| GROUP BY / HAVING | SELECT status, COUNT(*) FROM orders GROUP BY status |
| ORDER BY | SELECT * FROM products ORDER BY price ASC, including NULLS FIRST / NULLS LAST |
| Aggregate functions | SELECT COUNT(*), SUM(total) FROM orders |
| FILTER on aggregates | SELECT COUNT(*) FILTER (WHERE status = 'paid') FROM orders (also inside OVER (...)) |
| Window functions | SELECT id, name, ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) FROM employees |
| CASE expressions | SELECT CASE WHEN active THEN 'yes' ELSE 'no' END FROM users |
| Arithmetic expressions | SELECT price * quantity AS total FROM line_items |
| Typecasts | SELECT col::int FROM t, WHERE created_at = '2026-01-01'::date |
| Uncorrelated subqueries | WHERE id IN (SELECT user_id FROM orders), derived tables, scalar subqueries |
| CTEs | WITH active AS (SELECT * FROM users WHERE active) SELECT * FROM active |
| Set operations | SELECT id FROM users UNION SELECT id FROM admins |
| Functions in SELECT | SELECT COALESCE(name, 'unknown') FROM users |
| Immutable functions in WHERE | WHERE lower(name) = 'foo', WHERE abs(balance) > 100 |
| LIMIT / OFFSET | SELECT * FROM products ORDER BY price LIMIT 10 OFFSET 20 |
| LIKE / ILIKE | WHERE name LIKE 'foo%', WHERE name ILIKE '%bar%' |
| BETWEEN | WHERE price BETWEEN 10 AND 100, including SYMMETRIC |
| ANY / ALL | WHERE id = ANY(ARRAY[1, 2, 3]), WHERE score > ALL(ARRAY[80, 90]) |
| Mixed wildcards and columns | SELECT *, id, name FROM users |
| EXISTS / NOT EXISTS | WHERE EXISTS (SELECT 1 FROM orders WHERE orders.user_id = users.id) |
| Correlated subqueries | WHERE id IN (SELECT ...), NOT IN (SELECT ...), scalar subqueries, EXISTS/NOT EXISTS — decorrelated into equivalent JOINs |
| IS [NOT] TRUE/FALSE/UNKNOWN | WHERE active IS TRUE, WHERE flag IS NOT FALSE |
| Extended query protocol | Parameterized queries via Parse/Bind/Execute |
Not Cached (Forwarded to Origin)
| Pattern | Reason |
|---|---|
| FULL JOIN | Not yet supported for caching |
| LATERAL subqueries | Not supported |
| RECURSIVE CTEs | Not supported |
| Non-equality correlated predicates | Correlated subqueries using <, >, or OR-connected correlation are not decorrelated |
| Non-immutable functions outside SELECT list | Stable or volatile function calls — e.g. now(), random(), nextval() — in WHERE, JOIN, or other clauses (they are allowed in the SELECT list) |
| Locking clauses | FOR UPDATE, FOR SHARE, etc. are always forwarded to the origin |
| Views | Queries against views are forwarded to the origin |
| Extension and composite types | Tables with columns of an extension type (e.g. pgvector’s vector, PostGIS geometry) or a composite type are not cached — these types are not reproduced in the cache database, so such queries are forwarded to the origin |
| COPY | COPY … TO/COPY … FROM is forwarded to the origin and never cached. A COPY … FROM write still propagates to cached tables through CDC like any other write |
| Non-SELECT statements | INSERT, UPDATE, DELETE, DDL are always forwarded |
Queries that aren’t cached still work correctly — they pass through to the origin with no functional difference.
Client Library Compatibility
PgCache speaks the PostgreSQL wire protocol. Any client library that connects to PostgreSQL will work with PgCache without modifications.
| Language | Libraries |
|---|---|
| Python | psycopg2, psycopg3, asyncpg, SQLAlchemy |
| JavaScript/TypeScript | node-postgres (pg), Prisma, Drizzle, Knex |
| Java | JDBC, HikariCP, jOOQ |
| Go | pgx, database/sql with pq driver |
| Rust | tokio-postgres, sqlx, diesel |
| Ruby | pg gem, ActiveRecord |
| .NET | Npgsql, Entity Framework |
ORMs and query builders work as expected — PgCache analyzes the SQL they generate the same way it handles hand-written queries.
TLS Support
| Connection | Configuration |
|---|---|
| Client to PgCache | tls_cert and tls_key in config (PEM format) |
| PgCache to origin | ssl_mode = "require" or "verify-full" in [origin] section |
| PgCache to replication | ssl_mode = "require" or "verify-full" in [replication] section |
require encrypts the connection without verifying the server certificate (suitable for self-signed certs). verify-full additionally verifies the certificate against trusted public CAs.
For Docker deployments, TLS certificates can be passed as base64-encoded environment variables (PGCACHE_TLS_CERT, PGCACHE_TLS_KEY).
Note: Channel binding must be disabled for client connections to PgCache. Set channel_binding=disable in your client connection string or library configuration.
Connection Pooler Compatibility
PgCache works alongside connection poolers like PgBouncer. A common pattern is:
- Application connects to PgCache (caching proxy)
- PgCache connects to PgBouncer for regular queries (via
[origin]) - PgCache connects directly to PostgreSQL for CDC replication (via
[replication])
This is configured using the [replication] section to override the origin connection for logical replication, which requires a direct PostgreSQL connection.
[origin]
host = "pgbouncer.example.com"
port = 6432
user = "app_user"
database = "myapp"
[replication]
host = "postgres-direct.example.com"
port = 5432Known Limitations
- Row-Level Security (RLS): RLS is not yet supported. Until support lands, queries against tables with RLS enabled should be excluded from caching at the application or configuration level — pgcache does not currently detect RLS-enabled tables on its own, so cached results would not respect per-role policies.
- Read-after-write: Inside a transaction every query is forwarded to the origin, so a session always sees its own writes. Outside a transaction, cached reads reflect a write only after logical replication catches up (typical replication lag). See How Caching Works → Consistency Guarantees for the full model. A stronger read-after-write guarantee is in active development.
- Schema changes (DDL): PgCache tracks row changes, not DDL. Most schema changes are handled transparently, but dropping a table, or attaching or detaching a partition that already holds rows, is not observed via logical replication and can serve stale results for the affected tables.
- Cache reset on startup: The cache database is fully reset each time PgCache starts. This ensures a clean state but means the cache is cold after a restart.
- Single-node: PgCache runs as a single process with an embedded cache. There is no distributed cache or multi-node support.
- COPY protocol: The PostgreSQL COPY protocol is not proxied through PgCache.
- LISTEN/NOTIFY: PostgreSQL asynchronous notifications are not proxied.
- Replication management: PgCache creates and manages its own publication and replication slot on the origin. If PgCache is down for an extended period, WAL can accumulate on the origin. Monitor
pg_replication_slotson the origin.