MODX Performance Optimization
MODX Revolution is flexible, but without proper caching and server configuration easily hits TTFB 2–4 seconds even on simple sites. Reason—aggressive database work, parsing [[*]]/[[+]] tags on every request, and no built-in object cache.
Where Time is Lost: Query Profiling
First step—understand what exactly slows things down. MODX logs slow requests if enabled in core/config/config.inc.php:
define('MODX_CONFIG_KEY', 'config');
// in System Settings:
// log_level = 4 (DEBUG)
// log_target = FILE
More precise tool—xDebug + Blackfire or just EXPLAIN in MySQL/MariaDB for queries MODX generates during page render. Typical problems:
-
modResourcereads all fields includingcontenteven when onlypagetitleneeded -
getResourceswithoutlimitdoesSELECT *on all child resources - snippets without
&cache=1execute on every request
Caching at MODX Level
System cache stored in core/cache/. For production ensure directory has 755 permissions and isn't mounted via tmpfs without sufficient size.
Optimal settings in System Settings:
| Parameter | Recommended Value |
|---|---|
cache_resource_handler |
xPDOFileCache (default) or Redis |
cache_default_lifetime |
3600–86400 depending on update frequency |
cache_context_settings |
1 |
compress_js |
1 |
compress_css |
1 |
For Redis cache, install Redis package (modmore or similar) and in core/config/config.inc.php add:
$config_options = [
'cache_path' => MODX_CORE_PATH . 'cache/',
'cache_default_handler' => 'xPDORedisCache',
'cache_xpdo_handler' => 'xPDORedisCache',
'redis_host' => '127.0.0.1',
'redis_port' => 6379,
'redis_db' => 0,
];
After switching to Redis, TTFB drops 30–50% on high-traffic sites because disk stat() operations on cache files disappear.
Snippets and Chunks Optimization
Uncached calls [[!SnippetName]] are main source of slowness. Audit by searching templates and chunks:
grep -r '\[\[!' /var/www/modx/core/elements/ | grep -v '.svn'
# or in templates via MODX manager: Elements > Templates > Search
Rules:
- If snippet doesn't depend on session/cart/user—make cacheable
[[SnippetName? &cache=1&cacheExpires=3600]] -
getResources,pdoResourcescached via built-in&cacheparameter -
FormItandLogin—always uncached, put in separate chunks
pdoTools instead of getResources—critical for large catalogs. pdoTools uses JOIN instead of multiple queries:
[[pdoResources?
&parents=`15`
&depth=`2`
&limit=`20`
&sortby=`publishedon`
&sortdir=`DESC`
&cache=`1`
&cacheExpires=`1800`
]]
PHP-OPcache and Configuration
; /etc/php/8.1/fpm/conf.d/10-opcache.ini
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.validate_timestamps=0 ; disable on production
opcache.revalidate_freq=0
opcache.fast_shutdown=1
PHP-FPM pool for MODX:
[modx]
pm = dynamic
pm.max_children = 20
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 8
pm.max_requests = 500
request_terminate_timeout = 30s
pm.max_requests = 500—prevents memory leaks in long-lived snippets.
Nginx + Static Files + gzip
server {
# Static files with long cache
location ~* \.(js|css|png|jpg|webp|woff2|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# Deny direct access to core
location ^~ /core/ {
deny all;
}
# gzip
gzip on;
gzip_types text/plain text/css application/json application/javascript image/svg+xml;
gzip_min_length 1024;
gzip_comp_level 5;
}
Database Indexes
MODX doesn't add all needed indexes automatically. For 10,000+ resources add:
-- Speeds up selection by parent + status
ALTER TABLE modx_site_content
ADD INDEX idx_parent_published (parent, published),
ADD INDEX idx_context_published (context_key, published);
-- For date sorting
ALTER TABLE modx_site_content
ADD INDEX idx_publishedon (publishedon);
After adding indexes, EXPLAIN SELECT on typical getResources query shows ref instead of ALL.
Timeline
Basic optimization (cache, PHP-FPM, nginx, indexes): 2–3 days. Converting uncached snippets to cached + template audit: 3–5 days depending on element count. Switching to Redis cache with testing: 1 day additional.







