Bitdoze Logo

What Postgres Can Replace: Redis, Elasticsearch, MongoDB and More (2026)

Postgres can replace Redis, Elasticsearch, MongoDB, Pinecone, cron tools and light queues. Real SQL for each swap, honest limits, and what it still can't do.

DragosDragos9 min read
What Postgres Can Replace: Redis, Elasticsearch, MongoDB and More (2026)

My VPS used to run four services for one side project: Postgres for app data, Redis for sessions and rate limits, Meilisearch for product search, and a cron container that restarted things when they broke. Three of the four are gone now. Postgres does the work, and the project has been boring ever since, which is the highest compliment I can give infrastructure.

This is not a “Postgres can do everything” manifesto. Some of these swaps are free wins. Others have sharp limits that show up under load. For each one I’ll give you the mechanism, SQL you can run today, and the point where you should go back to the dedicated tool.

If you’re still deciding whether you need a server at all, read why a home server makes sense first. Everyone else, here’s the map.

Instead of Use in Postgres The catch
Redis (cache) UNLOGGED tables Wiped on crash, not on replicas
Redis (locks, counters) Advisory locks, atomic upserts Slower than memory
Pinecone, Qdrant pgvector + HNSW Index RAM above ~1M vectors
Elasticsearch (most search) tsvector + GIN, pg_trgm Weak relevance ranking
MongoDB JSONB + GIN One primary, no horizontal writes
Cron containers pg_cron Dies with the database
RabbitMQ (light use) SKIP LOCKED, pgmq Thousands per second, not millions
InfluxDB TimescaleDB Community license terms
Neo4j (light use) Apache AGE, recursive CTEs Rare on managed hosts
One-off ETL scripts Foreign data wrappers Not a real pipeline

1. Cache: UNLOGGED tables instead of Redis

Redis earns its keep at sub-millisecond scale. Below that, a table does the job.

UNLOGGED tables skip the write-ahead log, which is where Postgres pays for crash safety. Writes get faster, and you accept the trade: if the database crashes, Postgres truncates the table on restart. For a cache, that’s the correct behavior anyway. Nobody mourns a lost session cache.

CREATE UNLOGGED TABLE sessions (
  token text PRIMARY KEY,
  user_id int NOT NULL,
  expires_at timestamptz NOT NULL
);

Two limits before you migrate anything. First, UNLOGGED tables are not replicated, so the cache lives only on the primary. Second, a Postgres lookup crosses the network and the query planner, so you get single-digit milliseconds where Redis gives you microseconds. For session storage, response caching, and rate-limit counters on a self-hosted stack, that difference has never mattered on anything I run. It starts to matter around high traffic, and by then you can afford Redis.

2. Vector search: pgvector instead of Pinecone

pgvector adds a vector column type, distance operators, and indexes to Postgres. Embeddings live next to the rows they belong to, which kills the sync job between your database and a managed vector service.

CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE docs (
  id bigserial PRIMARY KEY,
  body text,
  embedding vector(1536)
);

CREATE INDEX ON docs USING hnsw (embedding vector_cosine_ops);

SELECT id, body FROM docs
ORDER BY embedding <=> $1
LIMIT 5;

The HNSW index handles approximate nearest-neighbor search and comfortably serves the RAG-sized datasets most projects actually have. Somewhere above a million vectors you start caring about index memory and build time, and that’s where pgvectorscale from Timescale or a dedicated engine earns its place back.

I keep a full setup guide for pgvector on Docker, compose file to HNSW index. If you’re building agents, the Mastra assistant guide wires retrieval into a working loop.

3. Full-text search: tsvector instead of Elasticsearch

Most projects that install Elasticsearch use about five percent of it. The other 95 percent is a document store with an index, which describes Postgres just as well.

The built-in engine parses text, drops stop words, and stems words to their roots, so a search for “running” matches “run”. A generated column keeps the index updated without triggers:

ALTER TABLE articles ADD COLUMN search tsvector
  GENERATED ALWAYS AS (
    to_tsvector('english', coalesce(title, '') || ' ' || coalesce(body, ''))
  ) STORED;

CREATE INDEX idx_articles_search ON articles USING gin (search);

SELECT title FROM articles
WHERE search @@ websearch_to_tsquery('english', 'postgres cache');

websearch_to_tsquery accepts the sloppy syntax users actually type, quotes and all.

Where Postgres loses: ranking. Elasticsearch scores with BM25 and gives you knobs for everything. ts_rank is cruder. For site search, admin panels, and “find my blog post” this is genuinely fine. If search is the product itself, look at ParadeDB’s pg_search, which puts BM25 inside Postgres, or accept that you need the dedicated tool.

4. Documents: JSONB instead of MongoDB

MongoDB’s original pitch was “schemaless”. JSONB gives you the same shape with transactions and joins attached, which turns out to be what people wanted anyway.

CREATE TABLE events (
  id bigserial PRIMARY KEY,
  payload jsonb NOT NULL,
  created_at timestamptz DEFAULT now()
);

CREATE INDEX ON events USING gin (payload);

-- every event whose payload contains {"type": "login"}
SELECT * FROM events WHERE payload @> '{"type": "login"}';

JSONB is stored in a parsed binary form, GIN indexes make containment queries fast, and you can index individual keys inside documents. What you don’t get is horizontal write scaling. One Postgres primary writes as fast as one server writes. For the document-shaped workloads of a typical app, that ceiling is far away. If you need the MongoDB wire protocol specifically, FerretDB and AWS’s open-sourced DocumentDB extension both sit on top of Postgres.

5. Cron: pg_cron instead of schedulers and glue scripts

pg_cron runs inside the database and speaks real cron syntax:

SELECT cron.schedule(
  'nightly-cleanup',
  '0 3 * * *',
  $$DELETE FROM sessions WHERE expires_at < now()$$
);

Every run and its errors land in cron.job_run_details, so debugging is a SELECT instead of a container log dive.

The underrated move is pairing it with pg_net, which fires async HTTP requests from SQL. Scheduled API pings, webhook retries, cache warmups: jobs that used to need a script container now need one line. One catch, and it’s an honest one: the scheduler lives where the database lives. A dead database stops your jobs. On a single VPS that was always true anyway; your cron container died with the same box.

6. Geospatial: PostGIS instead of a GIS engine

PostGIS is the oldest entry on this list and the least debated. It adds geometry and geography types, spatial indexes, and a few hundred functions.

CREATE EXTENSION IF NOT EXISTS postgis;

-- depots within 3 km of a point in Lisbon
SELECT name FROM depots
WHERE ST_DWithin(
  location::geography,
  ST_MakePoint(-9.14, 38.72)::geography,
  3000
);

The geography type does math on a spheroid, so distances come back in real meters instead of degrees. ST_Contains handles polygon checks for delivery zones or flood areas. The catch is operational: PostGIS is a big install, and managed providers each ship their own version. If your app needs “find stores near me”, plain lat/lng columns cover it. PostGIS is for when geometry gets serious.

More swaps, rapid fire

Queues: SKIP LOCKED instead of RabbitMQ

The whole worker pattern is one WHERE clause. FOR UPDATE SKIP LOCKED lets many workers pull different rows from the same table without stepping on each other. The pgmq extension wraps it into a proper message queue, and frameworks like pg-boss (Node), Oban (Elixir), and River (Go) already build job systems on this pattern. Honest ceiling: thousands of jobs per second, not hundreds of thousands.

Time-series: TimescaleDB instead of InfluxDB

TimescaleDB chunks big tables by time, compresses old chunks, and maintains continuous aggregates in the background. For server metrics, sensor data, and dashboards, a hypertable replaces an InfluxDB instance. RDS supports it. Read the community license terms before you build a product on it.

Locks and rate limits: advisory locks instead of Redlock

SELECT pg_advisory_lock(42) is a distributed lock with zero extra infrastructure. For rate limits, an atomic upsert counter with a timestamp column does what people deploy Redis for. Slower than memory, yes. Sufficient, usually.

Graphs: recursive CTEs and Apache AGE instead of Neo4j

Org charts, category trees, threaded comments: a recursive CTE does these with no extension at all. For real graph queries, Apache AGE adds openCypher to Postgres. Managed providers rarely ship it, so this swap usually means self-hosting.

Federated reads: FDWs instead of one-off ETL scripts

Foreign data wrappers let Postgres query other databases, CSV files, and Parquet on S3 as if they were local tables. postgres_fdw joins across databases, file_fdw reads logs. It won’t replace your data pipeline, but it retires a surprising number of export scripts.

Fuzzy matching: pg_trgm instead of a search service

Trigram similarity powers “did you mean” autocomplete with one GIN index and similarity(name, 'ibm') > 0.3. Typo tolerance for an admin panel does not need its own infrastructure.

What Postgres will not replace

Every “Postgres does everything” post skips this part, so here it is.

Object storage stays. Blobs in the database bloat backups, slow down vacuums, and cost more than S3 or B2. Keep files outside, keep the keys in Postgres.

Kafka-class throughput is out of reach. The SKIP LOCKED pattern serves thousands of messages per second. Kafka serves millions, with replay and partitions. Past that line they stop being the same tool.

Microsecond latency belongs to Redis. Postgres crosses a network and plans a query. When every millisecond costs real money, keep the cache in memory.

Relevance tuning is Elasticsearch’s home turf. Postgres search is good enough for most sites and not good enough for search-centric products. Typesense or Meilisearch also fit here.

Horizontal writes don’t come free. One Postgres primary writes with one machine’s budget. Citus and read replicas stretch that far, but MongoDB-style write scaling is a different architecture.

Check your provider's extension list first

Every swap above assumes you can enable the extension. Supabase and Neon ship most of them: pgvector, pg_cron, pg_net, pgmq. Amazon RDS has a shorter list and no pg_net or pgmq. Self-hosted Postgres has everything one CREATE EXTENSION away, which is part of why this pattern took off in homelab circles. Check before you design around a swap, not after.

Which swaps to actually make

Questions that come up

Is Postgres as fast as Redis?

No, and pretending otherwise would be dishonest. Redis answers in tens of microseconds from memory. Postgres answers indexed lookups in single-digit milliseconds. The useful question is whether your feature can tell the difference. Session validation during a page load can’t.

Can I migrate off Redis gradually?

Yes, and you should. The read-through pattern makes it boring: write new sessions to Postgres, keep reading old ones from Redis, and let TTLs expire the old store. No big-bang cutover, no rollback plan needed.

Does this work for local AI setups?

It’s arguably the best case for it. pgvector and Ollama on the same machine give you embeddings, semantic search, and RAG without any data leaving your network.

Start with the swap that annoys you most

None of this makes Redis or Elasticsearch bad tools. It means a default Postgres install covers more ground than most people expect, and every service you don’t run is one you don’t patch, back up, or pay for.

My breaking point was the cron container that kept dying silently. Yours might be a $50/month vector database bill or a Meilisearch instance nobody maintains. Check your provider’s extension list, run one of the queries above on a scratch table, and see if the boring database earns another job.

Este artículo también está disponible en español: Qué Puede Reemplazar Postgres.