Caching Phalcon ORM Query Results Without Breaking Freshness
Use Phalcon's inline ORM cache option to skip repeated database hydration for rarely changing data, with TTL and invalidation guidance.
12 Feb 2026, 19:40 UTC

If your Phalcon application reloads the same reference data on every page load, the database may not be your bottleneck—object hydration is. Each call to find() forces Phalcon to fetch raw rows and transform them into model instances. For data that changes infrequently, repeating that work wastes CPU and memory on every request.
Phalcon's ORM includes an inline cache option for find() and findFirst(). When enabled, Phalcon stores the hydrated result set in a cache backend such as Redis, Memcached, or APCu. Subsequent requests can skip both the SQL query and the hydration step entirely.
How the Inline Cache Option Works
Before caching works, you must register a cache service in Phalcon's dependency injection (DI) container. The DI container is the framework's central registry for shared objects. The service is conventionally named cache and must implement Phalcon\\Cache\\CacheAdapterInterface.
When you call Categories::find(['cache' => true]), Phalcon checks the cache service for a key derived from the model name and query parameters. On a hit, it returns the stored hydrated array without touching the database. On a miss, it runs the query, hydrates the models, stores the result, and returns it.
You can override the auto-generated key with a string or a callable. This is useful when you want a predictable cache identity across deploys or when query parameters alone would produce ambiguous keys.
Worked Example: Caching Active Product Categories
Imagine an e-commerce site where product categories change rarely but appear on nearly every page. First, register a cache service named cache in your DI container. The service must implement Phalcon\\Cache\\CacheAdapterInterface; the exact adapter class (Redis, Memcached, or APCu) depends on your infrastructure and Phalcon major version.
// Bootstrap: register the cache service. Syntax varies by Phalcon major version.
$di->set('cache', function () {
// Return an instance implementing Phalcon\\Cache\\CacheAdapterInterface
return new \\Your\\Selected\\CacheAdapter();
}, true);
// In a controller or service layer
$categories = \\Categories::find([
'conditions' => 'status = :status:',
'bind' => ['status' => 'active'],
'order' => 'name ASC',
'cache' => [
'key' => 'active-categories-v1',
'lifetime' => 3600,
],
]);
foreach ($categories as $category) {
echo $category->name;
}
Replace \\Your\\Selected\\CacheAdapter with the adapter for Redis, Memcached, or APCu that matches your installed Phalcon version. The query portion uses the inline cache option common to Phalcon 4.x and 5.x; Phalcon 3.x uses a different option signature, so consult the API documentation for your version.
Explicitly naming the key active-categories-v1 makes the cache entry easy to find and delete. If you omit key, Phalcon generates one from the model and parameters, which can create orphaned entries when similar queries differ only in bind values.
Trade-offs: Stale Data and Memory Pressure
Caching hydrated models introduces two engineering risks.
- Stale data: If a category is renamed or deactivated in the database, the cache still returns the old object until the TTL expires or you delete the key manually. Pair caching with invalidation logic in your admin or update endpoints.
- Memory pressure: Phalcon caches the full hydrated result set, not raw rows. Caching thousands of models can consume significant RAM in APCu or Redis. Limit caching to small, frequently read result sets.
For large lists, consider caching only the IDs or using raw SQL with a lighter serialization format instead of full model hydration.
Verifying That Caching Actually Happens
Run a minimal script in your application environment with the same user permissions as your web or CLI process. Do not run cache-warming scripts in production during peak traffic without load testing.
// First call: database hit, populates cache
$first = \\Categories::find([
'cache' => ['key' => 'verify-cache-hit', 'lifetime' => 60],
]);
// Second call: should return from cache, no new SQL
$second = \\Categories::find([
'cache' => ['key' => 'verify-cache-hit', 'lifetime' => 60],
]);
var_dump($first->count() === $second->count());
To confirm the database is bypassed, enable your database's slow-query log or run MONITOR in Redis if using the Redis backend. The second call should produce no new query against the categories table. Inspect the returned objects with Xdebug or var_dump to confirm they are model instances, not raw arrays.
This verification is a diagnostic check, not a production load test. Profile under realistic traffic before applying caching to additional queries.
Practical Closing
Start with one expensive, read-heavy query. Register a cache service, add an explicit key and TTL, and verify with a cache-hit check. Add manual invalidation wherever the underlying data can change. If memory usage grows or data freshness becomes critical, shrink the TTL or remove caching for that query.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.