Sitemap
Coding Algorithms

Coding Algorithms

ColdFusion Query Caching vs ORM vs qb: Which One Performs Better at Scale?

11 min readMay 28, 2026

--

Ask three ColdFusion developers which data-access approach is fastest and you’ll get four opinions and a flame war. One swears by cachedWithin. One built the whole app on Hibernate-backed ORM and won't hear otherwise. The third installed qb last quarter and now writes everything as fluent chains. At low traffic, all three feel fine. The question only gets interesting under load — and that's exactly where the marketing answers fall apart.

So let’s settle it honestly. Not with invented benchmark numbers, but with how each approach actually behaves when concurrency rises and data volume grows.

TL;DR: There’s no single winner, because these three things don’t optimize for the same problem. Native query caching is the fastest for stable, read-heavy data — it removes the database round-trip entirely. ORM (Hibernate) optimizes developer productivity and object modeling, but introduces N+1 query risk and session-management overhead that bite hardest at scale. qb sits closest to raw SQL performance because it compiles to parameterized queryExecute calls — the runtime cost is the CFML compilation step, not the database. The real lesson: caching is not a competitor to qb or ORM. It's a layer you add to either.

First, the Clarification Most “Comparisons” Get Wrong

The framing “caching vs ORM vs qb” is slightly miscast, and understanding why is half the battle.

Query caching is a result-storage strategy. It answers: “Do I even need to hit the database for this?”

ORM is a data-access paradigm. It maps database rows to CFC objects and back, and generates the SQL for you via Hibernate.

qb is a query-construction tool. It builds parameterized SQL through a fluent API and runs it through queryExecute.

Here’s the part that matters: ColdFusion’s native query caching can be layered onto a raw cfquery, onto a queryExecute call, and onto a qb query — because qb's execution methods accept the same options struct you'd pass to queryExecute. Hibernate ORM has its own separate caching layer (the secondary cache). So the honest question isn't "which one is fastest," it's "which paradigm fits my workload, and how do I cache within it."

With that cleared up, let’s look at each one under scale.

ColdFusion Native Query Caching at Scale

How It Works

ColdFusion stores query result sets in server memory. The first time the query runs, results are cached; every subsequent identical request reads from memory instead of the database. Per Adobe’s official cfquery documentation, you control this with the cachedWithin attribute (and related attributes cachedAfter, cacheID, cacheRegion, and cacheMaxIdleTime).

<!--- Reference data that rarely changes: ideal cache candidate --->
<cfquery name="getCountries" datasource="appdb"
cachedWithin="#createTimeSpan(0, 1, 0, 0)#">
SELECT country_id, country_name
FROM countries
ORDER BY country_name
</cfquery>

In script syntax, the same idea through queryExecute:

<cfscript>
qryCountries = queryExecute(
"SELECT country_id, country_name FROM countries ORDER BY country_name",
{},
{ cachedWithin = createTimeSpan(0, 1, 0, 0) }
);
</cfscript>

For arbitrary data (not just queries), ColdFusion provides cachePut / cacheGet, and cfcache for caching entire rendered page fragments.

Where It Wins at Scale

For read-heavy, low-volatility data — country lists, category trees, configuration tables, pricing tiers — caching is unbeatable, because the fastest database query is the one you never run. Under heavy concurrent load, a cached query serves thousands of requests from RAM without touching the connection pool. That’s the single biggest lever most ColdFusion apps leave unpulled.

Where It Bites at Scale

The trap is documented and brutal: ColdFusion caches result sets based on the exact SQL statement. A dynamic query with a parameter creates a separate cache entry for every distinct value. Cache a per-user query and you’ll fill the heap with thousands of near-identical result sets, triggering GC pressure and cache eviction churn — the opposite of the performance you wanted.

Two more scale-specific gotchas:

  • The cache has a size limit. Historically the default cached-query ceiling has been low (on the order of 100 queries), configurable in the ColdFusion Administrator. Exceed it and older entries get evicted, quietly killing your hit rate.
  • Stale data is a correctness bug, not just a performance one. Caching the wrong thing — inventory, auth state, anything that changes — produces wrong answers that look fine in testing. CF10 added removeCachedQuery() to invalidate specific entries, and Ehcache-backed query caching (with cache.useInternalQueryCache and cache.querysize settings in Application.cfc) for finer control.

Verdict: Caching is the highest-ROI optimization for the right data. It’s a footgun for volatile or highly-parameterized data.

ColdFusion ORM (Hibernate) at Scale

How It Works

ColdFusion ORM is built on Hibernate. You define persistent CFCs, and the engine generates SQL, manages object state, and maps results to objects.

// Author.cfc
component persistent="true" table="authors" {
property name="authorId" fieldtype="id" generator="identity";
property name="name";
property name="posts" fieldtype="one-to-many"
cfc="Post" fkcolumn="author_id" lazy="true";
}

// Loading an entity
author = entityLoad("Author", { authorId = 5 }, true);

The N+1 Problem — The ORM Scale Killer

This is the single most important thing to understand about ORM at scale, and it comes straight from Adobe’s own documentation. By default, ColdFusion ORM lazy-loads relationships — when you load an author, their posts aren’t fetched until you call the getter. That sounds efficient, and often is.

But the eager-fetch alternative, per Adobe’s documentation, is “extremely vulnerable to the N+1 Select problem.” Picture loading 100 authors and then looping to display each author’s posts: you fire 1 query for the authors, then 100 more — one per author — for their posts. 101 queries where 2 would do. At 10 authors in dev, you never notice. At 10,000 under load, you’ve melted the database.

The fixes Hibernate and ColdFusion ORM provide are real but require deliberate use: batch fetching (batchsize), join fetching, subselect fetching, and paging via ORMExecuteQuery with offset and maxresults. None are automatic. ORM gives you the rope; you decide whether to climb or hang.

ORM’s Secondary Cache

ORM doesn’t use ColdFusion’s cachedWithin query cache. It has its own secondary cache, configured in Application.cfc:

this.ormSettings = {
secondarycacheenabled = true,
cacheprovider = "ehcache",
cacheconfig = "/config/ehcache.xml"
};

Then mark cacheable entities with a cacheuse strategy. The documented strategies — and this matters at scale — are read-only (fastest, only for immutable data), nonstrict-read-write, read-write (which Hibernate's own docs note is the worst-performing of the writable strategies), and transactional.

The Session-Management Tax

ORM’s Hibernate session opens on the first CRUD call and closes at the end of the request. Access a lazy-loaded relationship after the session has closed — a common mistake when stashing entities in session or application scope — and you get the detached-object error that Hibernate developers know too well. At scale, this manifests as intermittent, hard-to-reproduce failures.

Verdict: ORM is a productivity multiplier and models complex object graphs cleanly. Its scale performance is entirely a function of whether the team understands fetch strategies and session lifecycle. Misused, it’s the slowest of the three. Used well — with batch fetching and a secondary cache — its read performance is competitive. The cborm module (ColdBox ORM) adds active-record patterns, virtual services, and criteria queries on top.

qb (Query Builder) at Scale

How It Works

qb is a fluent query builder by Ortus Solutions, heavily inspired by Laravel’s Eloquent and part of the ColdBox ecosystem. It’s MIT-licensed and installs via CommandBox (box install qb). Critically for a performance discussion: qb compiles your fluent chain into a parameterized SQL string and executes it through queryExecute.

// qb
query = wirebox.getInstance("QueryBuilder@qb");
q = query.from("posts")
.whereNotNull("published_at")
.whereIn("author_id", [5, 10, 27])
.get();

// Compiles to roughly:
// SELECT * FROM posts
// WHERE published_at IS NOT NULL AND author_id IN (?, ?, ?)

Because the output is just a parameterized query run through the native engine, qb’s database execution performance is essentially the same as hand-written queryExecute. The only overhead qb adds is the CFML cost of building and compiling the query object — pure application-server work, not database work. For the vast majority of requests, that compilation cost is negligible compared to the query's actual database time.

The One Real Performance Caveat — And It’s Documented

Per qb’s official installation documentation: qb binds all parameters by default and guesses the SQL type from the passed value. The default SQL type for numeric values is CF_SQL_NUMERIC (a floating-point type) for maximum compatibility — and the docs explicitly warn this "can cause performance problems with large recordsets in some database engines."

Get Coding Algorithms’s stories in your inbox

Join Medium for free to get updates from this writer.

This is the kind of honest caveat that matters at scale. The fix is to set explicit SQL types or override the default in your ColdBox config:

// Be explicit about integer keys on large result sets
q = query.from("orders")
.where("customer_id", "=",
{ value = arguments.customerId, cfsqltype = "CF_SQL_INTEGER" })
.get();

Layering Caching onto qb

Here’s the payoff of the earlier clarification. qb’s get() (and every execution method) accepts "any additional queryExecute options" — the same options struct. That means you can pass cachedWithin straight through:

q = query.from("countries")
.orderBy("country_name")
.get(options = { cachedWithin = createTimeSpan(0, 1, 0, 0) });

So “qb vs caching” is a false choice. You get qb’s clean composition and native query caching at the same time.

Where It Wins at Scale

qb shines on dynamic, complex, multi-condition queries that would be a tangle of string concatenation in raw CFML — exactly the queries that grow with a maturing enterprise app. It abstracts grammar differences across MySQL, PostgreSQL, SQL Server, Oracle, SQLite, and Derby, which de-risks database migrations. And toSQL(showBindings = true) lets you inspect the exact generated SQL, so there's no "magic" hiding your query plans from you.

Verdict: qb gives you near-raw performance with dramatically better maintainability for dynamic SQL, and it composes cleanly with caching. Its overhead is application-side compilation, not database execution. Mind the default numeric type on large result sets.

Head-to-Head: Performance at Scale

A benchmark race would be dishonest — performance depends entirely on your data, queries, and traffic shape, and no credible public head-to-head of all three exists. What can be stated accurately is how each behaves architecturally:

Dimension Query Caching ORM (Hibernate) qb DB execution speed Zero (served from RAM on hit) Generated SQL; risk of N+1 ~Equal to raw queryExecute Best for Stable, read-heavy data Complex object graphs, CRUD-heavy apps Dynamic, complex SQL composition Main scale risk Cache bloat on dynamic queries; stale data N+1 queries; session lifecycle errors App-side compile cost; default numeric type Caching model Is the caching Separate secondary cache (Ehcache) Inherits native query caching via options Tuning required Pick cacheable data wisely Fetch strategy + secondary cache Explicit SQL types on big result sets Maintainability Attribute-level, simple High abstraction, steep when misused High — fluent, inspectable SQL

When Each One Wins

  • Reach for query caching when the data is read far more than it’s written and tolerates being seconds-to-hours stale: lookups, catalogs, config, navigation.
  • Reach for ORM when your domain is genuinely object-shaped, you’re doing heavy CRUD, and your team understands fetch strategies. Pair it with the secondary cache and batch fetching, or it will be the slowest option here.
  • Reach for qb when queries are dynamic and complex, when you want database portability, and when you want raw-query performance without raw-string maintenance pain.

The Pattern That Actually Scales

The teams that win don’t pick one. They layer:

  1. qb (or raw queryExecute) as the default for reads and dynamic queries — predictable, inspectable, fast.
  2. Native query caching on top of the stable subset — country lists, categories, config — via the cachedWithin option, whether the query is raw or built with qb.
  3. ORM where the domain is object-shaped and CRUD-heavy, with lazy loading by default, batch fetching for known collections, and the secondary cache enabled for reference entities.
  4. A measurement tool — like FusionReactor — watching the whole thing, so you can see cache hit ratios, spot N+1 query storms in the JDBC view, and confirm a “fast” query isn’t being run 10,000 times under load.

That last point is the one teams skip. Without production-level visibility into which queries fire how often, you’re optimizing blind — and the N+1 problem in particular is nearly invisible until you see the JDBC request count under real concurrency.

When to Bring in ColdFusion Specialists

Some of this you can tune yourself over a weekend. Some of it is architectural and expensive to get wrong:

  • Migrating a legacy app from raw cfquery to qb or ORM — a structural change that needs a plan, not a find-and-replace.
  • Diagnosing why an ORM-heavy app falls over under load — almost always N+1 plus session-lifecycle issues, and finding them requires reading generated SQL against production traffic.
  • Designing a caching strategy that doesn’t go stale or bloat the heap — deciding what’s safe to cache, for how long, and how to invalidate it is genuinely hard.
  • Database portability projects where qb’s grammar abstraction is part of a larger CF-to-cloud or CF-to-Lucee modernization.

For businesses evaluating ColdFusion development services or modernization, the signal of a good partner is simple: they measure before they prescribe. Anyone who recommends “rewrite it all in ORM” or “just cache everything” without first profiling where your request time actually goes is selling a template, not a solution.

Frequently Asked Questions

Does qb hurt performance compared to raw queries? At the database level, no — qb compiles to a parameterized query and runs it through queryExecute, so the database does the same work. qb adds a small CFML-side cost to build and compile the query object, which is typically negligible against actual query time. Watch the default CF_SQL_NUMERIC type on very large result sets; the qb docs note it can cause issues in some database engines.

Can I use ColdFusion query caching with qb? Yes. qb’s execution methods accept the same options struct as queryExecute, so you can pass cachedWithin directly to get(). Caching is not an alternative to qb — it layers onto it.

Is ORM always slower than raw queries or qb? No, but it’s the easiest to make slow. The N+1 select problem and Hibernate session-lifecycle issues are the usual culprits. With lazy loading, batch fetching, and the secondary cache configured correctly, ORM read performance can be competitive. Adobe’s own documentation calls eager fetching “extremely vulnerable to the N+1 Select problem,” which is why fetch strategy matters so much.

What’s the difference between ColdFusion’s query cache and ORM’s secondary cache? They’re separate systems. The native query cache (cachedWithin) stores result sets keyed by the exact SQL statement. ORM's secondary cache (Ehcache, enabled via secondarycacheenabled) caches entities and collections at the object level with strategies like read-only and read-write. They don't share storage.

Which scales best for a high-traffic read-heavy app? For the stable portion of the data, native query caching wins decisively — a cache hit means zero database work. For the dynamic portion, qb gives you the cleanest path to fast, maintainable queries. ORM fits the CRUD-heavy, object-modeled portions. Most large apps use all three deliberately.

Do these work on Lucee and BoxLang too? Native query caching and ORM are CFML engine features (Adobe ColdFusion and Lucee both support them, with some configuration differences). qb is an Ortus module supporting Adobe ColdFusion and Lucee across multiple database grammars; check the current qb documentation for the exact supported engine versions, as these have changed across qb releases.

The Bottom Line

“Which performs better at scale” is the wrong question, and the honest answer is more useful than a fake benchmark: they solve different problems, and the fastest production ColdFusion apps use all three on purpose.

Native query caching is the highest-impact optimization you have — for data that’s read often and changes rarely. qb gives you raw-query performance with sane, portable, inspectable SQL, and it inherits caching for free. ORM is a powerful abstraction whose scale behavior lives or dies by your fetch strategy and session discipline.

Pick the tool to the shape of the data, layer caching onto the stable parts, and — most importantly — measure what’s actually happening under load. The slow query you’re guessing about is almost never the one that’s actually killing you.

Sources Referenced For This Article

  • Adobe ColdFusion documentation: cfquery tag reference (cachedWithin, cachedAfter, cacheID, cacheRegion), removeCachedQuery, Optimizing ColdFusion applications, ORM Lazy Loading and Performance Optimization, ORM Settings reference
  • Ortus / qb official documentation (qb.ortusbooks.com): Introduction, Installation & Usage, Retrieving Results, Query Options and Utilities, Return Format, Debugging, What’s New
  • qb GitHub repository (coldbox-modules/qb)
  • Hibernate ORM reference documentation (Chapter 20: Improving Performance)
  • CFDocs: ORM Settings Struct
  • Raymond Camden: ColdFusion updated, and some notes about query caching
  • Sam Farmer: ColdFusion 9 ORM secondary cache
  • Compound Theory: ColdFusion 9 ORM — Explaining Hibernate Object State
  • learncfinaweek.com: Caching section

--

--