When NoSQL Actually Wins: The Real Trade-offs Between MongoDB and Postgres in Production

When NoSQL Actually Wins: The Real Trade-offs Between MongoDB and Postgres in Production

A pragmatic deep dive into when document databases genuinely beat relational databases in mature production systems, beyond the hype and marketing spin.

The “flexible schema” argument for MongoDB has always been a marketing Trojan horse. It’s been sold as a feature that frees you from the tyranny of migrations, but what it actually delivers is a deferred tax on your application logic that compounds with every passing sprint.

Let’s cut through the noise. The default-to-Postgres crowd has been winning the argument for years, and for good reason. But they’ve also been guilty of a certain dogmatic blindness. There are real, measurable scenarios where MongoDB genuinely outperforms PostgreSQL in production, and pretending otherwise is just as dangerous as the NoSQL hype train that burned so many teams in the 2010s.

The “Flexible Schema” Lie You Need to Stop Believing

One of the highest-rated comments on the post that inspired this article put it best: describing schema flexibility as a feature is “tantamount to saying ‘my car has no seatbelts, makes it so convenient to get in and out’.”

That’s not just a clever analogy, it’s a direct indictment of the NoSQL sales pitch. When you store documents without a schema, your application code becomes the schema. Every read operation becomes a negotiation with the past. You end up littering your codebase with if/else branches and default value assignments for fields that may or may not exist depending on when a document was written.

The comparison to dynamic typing is apt. We spent a decade rediscovering that dynamically typed languages needed type checkers (TypeScript, mypy, Pyright) to keep large codebases sane. MongoDB is undergoing the exact same reckoning, it now offers schema validation, but most teams never bother to maintain it. The parallel between untyped codebases with no compiler help and untyped databases with no schema help is almost too perfect to ignore.

This is not an argument against ever using MongoDB. It’s an argument against using it by default for data you care about.

JSONB: The Elephant in the Room That Everyone Ignores

PostgreSQL introduced JSONB in version 9.4, that was 2014. Twelve years of production hardening later, it’s embarrassing how many teams reach for a separate document database without understanding what their existing relational database can already do.

JSONB gives you:

  • Schema-flexible documents stored in a decomposed binary format optimized for query performance
  • GIN indexes with jsonb_path_ops that support containment (@>), existence (?), and key/value queries at scale
  • Partial indexes that index only documents matching a predicate, a feature MongoDB lacks at the same level of granularity
  • Full ACID transactions across both structured columns and unstructured JSONB fields, something MongoDB only got in version 4.0 and still has sharp edges with in sharded clusters

Let’s be concrete. Here’s how you’d model a flexible user profile in PostgreSQL with JSONB:

CREATE TABLE users (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email       TEXT UNIQUE NOT NULL,
    doc         JSONB NOT NULL,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- GIN index supporting containment queries across the whole document
CREATE INDEX idx_users_doc ON users USING GIN (doc jsonb_path_ops);

-- Targeted B-tree index on an extracted field for equality lookups
CREATE INDEX idx_users_locale ON users ((doc->'profile'->>'locale'));

-- Partial index, only active beta users
CREATE INDEX idx_users_beta ON users ((doc->'profile'->>'locale'))
  WHERE doc @> '{"profile": {"tags": ["beta"]}}';

This hybrid model, promoting stable, frequently-queried fields to first-class columns while keeping the flexible rest in JSONB, is usually the sweet spot. You get referential integrity, proper constraints, and the ability to run actual joins, all while maintaining the document flexibility that teams claim they need.

And the query translations are straightforward:

-- MongoDB: db.users.find({ "profile.locale": "en-GB" })
SELECT * FROM users WHERE doc->'profile'->>'locale' = 'en-GB';

-- MongoDB: db.users.find({ "profile.tags": "beta" })
SELECT * FROM users WHERE doc @> '{"profile": {"tags": ["beta"]}}';

-- MongoDB: $lookup across two collections
SELECT u.email, o.doc->>'total' AS order_total
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.doc->'profile'->>'locale' = 'en-GB';

If you’re still reaching for MongoDB after seeing this, you either have a very specific scaling requirement or you haven’t actually evaluated the JSONB option.

Where MongoDB Genuinely Wins: The Scaling Ceiling

Here’s where the conversation gets interesting. PostgreSQL is better in almost every conceivable way, until you hit a certain scale point. That scale point is higher than most people realize, but it’s real.

MongoDB’s built-in horizontal sharding is the one feature that PostgreSQL cannot easily replicate. When you need to distribute writes across multiple nodes without building your own sharding layer, MongoDB’s architecture gives you something that PostgreSQL simply doesn’t offer natively.

One engineer on the thread described it perfectly: MongoDB scales more painlessly because it’s “designed to partition internally so it handles near-infinite volumes and IOPS with the pull of a slider (and the accompanying month bill of course).”

The counter-argument that “that scale point is way higher than most people realize” is simultaneously true and infuriatingly dismissive. The people who actually need to have these conversations are the ones pushing the envelope. If you’re not scaling beyond a single Postgres instance, you don’t need to care about 90% of software architecture discussions. You can copy-paste a textbook solution and be fine.

But when you do hit that wall, when your Postgres instance is sweating under write-heavy workloads and you’re looking at Citus or manual partitioning, MongoDB’s auto-sharding starts looking very attractive. The operational complexity of Postgres sharding is real, and teams that have been through it often emerge with a grudging respect for MongoDB’s approach.

The Operational Reality: Schema-on-Read Maintenance Nightmare

Let’s talk about the part that doesn’t make it into the marketing brochures: maintaining un-migrated NoSQL documents in production.

When you have a million documents and you add a new field, you have three choices:

  1. Write a migration script that touches every document, defeating the purpose of “schema flexibility”
  2. Handle the missing field in application code, creating an ever-growing web of conditionals
  3. Ignore old documents, ensuring your data quality degrades over time

Teams that choose option 2 (the most common path) end up with code that looks like this scattered across their codebase:

def get_user_display_name(user_doc):
    # Old documents don't have displayName field
    if 'displayName' in user_doc:
        return user_doc['displayName']
    # Even older documents just had firstName + lastName
    if 'firstName' in user_doc and 'lastName' in user_doc:
        return f"{user_doc['firstName']} {user_doc['lastName']}"
    # Ancient documents only had name
    if 'name' in user_doc:
        return user_doc['name']
    # We give up
    return 'Unknown User'

This is not flexibility. This is deferred complexity with interest.

The comparison to dynamically typed languages is worth revisiting here. The move from SQL to NoSQL and back again mirrors exactly how developers got hyped on dynamic typing, then started adding type checking toolchains when they realized that untyped codebases with no compiler help are not very pleasant to work with over time.

PostgreSQL’s JSONB gives you the flexibility without the maintenance nightmare. You can add fields to your JSONB column without migrations, but when you need to enforce constraints, you have CHECK constraints, NOT NULL on extracted columns, and the full power of a mature RDBMS.

The Cost Reality Check

Let’s put some numbers on this. A production MongoDB Atlas cluster (M30 tier) costs approximately $280/month for 8GB RAM, 2 vCPUs, and 40GB storage. A comparable managed PostgreSQL instance on a European provider costs €19.99, €79.99/month for the same capacity.

The difference is not small. It’s an order of magnitude.

And that’s just the infrastructure cost. The operational cost of maintaining MongoDB’s sharding infrastructure, dealing with replica set elections, and managing the backup/restore toolchain is significantly higher than PostgreSQL’s equivalent. PostgreSQL’s tooling is more mature, better documented, and has a larger community of engineers who know how to operate it.

Criterion MongoDB Atlas M30 Managed PostgreSQL (EU)
Monthly cost ~$280 €19.99, €79.99
EU data residency Yes (US parent company) Yes (fully sovereign)
CLOUD Act exposure Yes (Nasdaq-listed parent) No
License SSPL (not OSI-approved) PostgreSQL License (OSI)
Full-text search Atlas Search (separate cluster) pg_trgm + tsvector (in-database)
Vector search Atlas Vector Search (separate) pgvector (in-database)
Managed backups Yes Yes
Operational burden Low-Medium Low

The pricing escalation is aggressive. Add Atlas Search nodes ($0.10, $0.30/hr each), Vector Search, online archive, backup retention tiers, and egress costs, and a real production MongoDB deployment lands comfortably north of $1,000, $5,000/month. At those numbers, the honest question becomes: what am I actually getting that I couldn’t get for one-tenth the price elsewhere?

When MongoDB Makes Sense

Despite the tone so far, I’m not arguing MongoDB has no place. There are genuine use cases where it wins:

  1. Write-heavy workloads at massive scale where MongoDB’s auto-sharding genuinely simplifies operations versus Postgres with Citus or manual partitioning
  2. Event sourcing and activity log systems where you’re capturing arbitrary, unpredictable data shapes and rarely querying them relationally
  3. IoT sensor data ingestion where you don’t know the schema upfront and need to capture whatever sensors throw at you, then extract patterns and define a schema later
  4. Mobile offline sync where Realm Sync or Atlas App Services provide functionality that PostgreSQL doesn’t have a direct equivalent for
  5. Legacy codebases where MongoDB drivers are deeply baked into your stack and a migration would cost more than the operational overhead

But for the other 80% of workloads, the SaaS applications, content management systems, e-commerce platforms, and internal tools, PostgreSQL with JSONB is almost always the better choice. It’s cheaper, more mature, has better tooling, and forces you to think about data integrity rather than deferring it.

The Real Decision Framework

Stop treating this as a binary choice. Almost all mature systems end up using both relational and document stores for different problems.

The decision framework should be:

  • Do you know your schema? Use PostgreSQL with proper columns.
  • Is your schema mostly stable with some variable fields? Use PostgreSQL with JSONB.
  • Is your schema completely unpredictable and you’re ingesting at massive write throughput? Consider MongoDB.
  • Are you scaling past a single node for write-heavy workloads? MongoDB’s auto-sharding starts looking attractive.
  • Do you need strict ACID across multiple entities? PostgreSQL wins every time.

The teams that suffer most are the ones that pick MongoDB because “we might need flexible schema someday” or because “PostgreSQL can’t scale.” The first reason is premature abstraction, and the second is a myth that’s been thoroughly debunked by real-world workloads.

MongoDB’s documented advantages, no translation pipeline, flexible schema, single-document atomic writes, built-in horizontal sharding, are real. But they come with costs that are rarely discussed: schema-on-read maintenance complexity, SSPL licensing concerns, aggressive pricing escalation past the free tier, and the operational burden of managing a secondary database system.

PostgreSQL’s JSONB has effectively neutralized the first three advantages. The only remaining differentiator is horizontal sharding, and that’s only relevant at a scale that most applications will never reach.

The trap of premature scaling and over-engineering database infrastructure is real. Before you reach for MongoDB because “we might scale to millions of users”, ask yourself honestly: how likely is that? And even if it happens, would a well-indexed, properly configured PostgreSQL instance with read replicas be sufficient?

For most teams, the answer is yes. And the savings in complexity, cost, and maintenance will be substantial.

The database you choose should be based on your actual requirements, not the imagined future load that might never materialize. Database scaling is a tax on simplicity, not a feature you implement upfront.

Use the right tool for the job. Just make sure you’ve actually evaluated all the tools before making that call.

Share:

Related Articles