Skip to content

Drupal Performance Optimization — Caching, CDN and Server Tuning

DodaTech Updated 2026-06-27 10 min read

In this tutorial, you'll learn how to optimize Drupal performance: configuring caching subsystems, integrating Varnish for HTTP caching, setting up CDN with Cloudflare or Fastly, tuning database queries, optimizing PHP configuration, and using monitoring tools like New Relic.

What You'll Learn

  • Drupal cache subsystems: Page, Dynamic Page, Render, Views, Block, Entity, Twig, PHP OPcache
  • Varnish integration with the Purge module and ban expressions
  • CDN integration with Cloudflare, Fastly, and Akamai
  • BigPipe optimization for Lazy Loading blocks
  • Database optimization: Solr/Elasticsearch, MySQL query tuning, Devel query logging
  • PHP tuning: OPcache, memory_limit, max_execution_time, realpath_cache
  • Web server tuning: Nginx fastcgi_cache, Apache mod_cache, KeepAlive
  • Image optimization: ImageAPI, WebP, lazy loading
  • CSS/JS aggregation and bandwidth optimization
  • Profiling with WebProfiler, XHProf, and New Relic
  • Load testing with Apache Bench, Siege, JMeter

Why It Matters

A slow Drupal site loses visitors, ranks lower in search results, and costs more in server resources. Drupal's caching system is powerful but requires intentional configuration. Without caching enabled, each page request loads dozens of modules, runs multiple database queries, and renders Twig templates from scratch. With proper caching, the same page loads in milliseconds. For sites with thousands of concurrent users, performance optimization is the difference between a usable site and a timeout error.

Real-World Use

A news website with 2 million monthly visitors runs on Drupal. Without optimization, page load time was 6 seconds. After enabling page caching, views caching, and BigPipe, load time dropped to 1.5 seconds. After adding Varnish and a CDN, time to first byte dropped to 200 milliseconds. The site now handles traffic spikes during breaking news without crashing. The optimization paid for itself in reduced server costs and higher ad revenue from improved engagement.

Learning Path

flowchart LR
  A[Migrate API] --> B[Performance Optimization]
  B --> C[Cache Subsystems]
  C --> D[Varnish and CDN]
  D --> E[BigPipe Optimization]
  E --> F[Database Tuning]
  F --> G[PHP and Server Tuning]
  G --> H[Monitoring and Testing]
  H --> I[Backups and Go-Live]

Drupal Cache Subsystems

Drupal has multiple caching layers. Understanding each one helps you configure them correctly.

Page Cache

Caches the entire HTML output for anonymous users:

<?php
// settings.php: enable page cache
$settings['cache']['bins']['page'] = 'cache.backend.database';
$config['system.performance']['cache']['page']['use_internal'] = true;
$config['system.performance']['cache']['page']['max_age'] = 21600; // 6 hours

Dynamic Page Cache

Caches pages for authenticated users, excluding personalized content:

<?php
// settings.php: enable dynamic page cache
$settings['cache']['bins']['dynamic_page_cache'] = 'cache.backend.database';

Render Cache

Caches rendered entities and individual render elements:

<?php
// Render cache is enabled by default
// It stores rendered output of entities, blocks, etc.

Views Cache

Views caching stores the query results and rendered output:

# Configure Views caching at /admin/structure/views
# Settings per display:
# - None: no caching (slowest)
# - Tag-based: invalidates when related content changes
# - Time-based: expires after N seconds
<?php
// Programmatically set views cache
$view = \Drupal\views\Views::getView('articles');
$view->setDisplay('page_1');
$view->display_handler->overrideOption('cache', [
  'type' => 'tag',
]);

Block Cache

Each block can be cached independently:

<?php
// In a custom block plugin
class MyBlock extends BlockBase {
  public function build() {
    return [
      '#markup' => $this->t('Hello!'),
      '#cache' => [
        'max-age' => 3600,
        'contexts' => ['user.roles'],
        'tags' => ['node_list'],
      ],
    ];
  }

  public function getCacheMaxAge() {
    return 3600;
  }
}

Twig Cache

Twig templates are compiled to PHP and cached:

<?php
// settings.php: Twig caching for production
$settings['twig_debug'] = false;
$settings['cache']['bins']['render'] = 'cache.backend.database';

PHP OPcache

OPcache caches compiled PHP scripts. Enable it in php.ini:

; php.ini
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.revalidate_freq=60
opcache.validate_timestamps=0  ; Production: disable validation

Varnish Integration

Varnish is an HTTP accelerator that sits in front of Drupal:

# default.vcl - Varnish configuration for Drupal
vcl 4.0;

backend default {
  .host = "127.0.0.1";
  .port = "8080";
}

sub vcl_recv {
  # Only cache GET and HEAD requests
  if (req.method != "GET" && req.method != "HEAD") {
    return (pass);
  }

  # Bypass cache for authenticated users
  if (req.http.Cookie ~ "SESS") {
    return (pass);
  }

  # Remove all cookies except session
  if (req.http.Cookie) {
    set req.http.Cookie = "";
  }

  # Strip query strings for static files
  if (req.url ~ "\.(css|js|png|jpg|jpeg|gif|ico|svg|webp)$") {
    unset req.http.Cookie;
  }
}

sub vcl_backend_response {
  # Set TTL for uncacheable items
  if (beresp.ttl <= 0s) {
    set beresp.ttl = 120s;
  }

  # Cache static files for 30 days
  if (bereq.url ~ "\.(css|js|png|jpg|jpeg|gif|ico|svg|webp)$") {
    set beresp.ttl = 30d;
  }
}
# Install the Purge module for cache invalidation
composer require drupal/purge drush/drush
drush pm:enable purge purge_drush

# Configure purge processors
drush config:set purge.plugins core '{"plugin_id": "core"}'

CDN Integration

Cloudflare

# Install Cloudflare module
composer require drupal/cloudflare
drush pm:enable cloudflare
<?php
// settings.php: Cloudflare configuration
$settings['reverse_proxy'] = true;
$settings['reverse_proxy_addresses'] = [
  '173.245.48.0/20',
  '103.21.244.0/22',
  '103.22.200.0/22',
  // Full list at https://www.cloudflare.com/ips
];
$settings['reverse_proxy_trusted_headers'] =
  \Symfony\Component\HttpFoundation\Request::HEADER_X_FORWARDED_FOR |
  \Symfony\Component\HttpFoundation\Request::HEADER_X_FORWARDED_PROTO;

Fastly

composer require drupal/fastly
drush pm:enable fastly

Configure Fastly service ID and API key at /admin/config/services/fastly.

BigPipe Optimization

BigPipe loads page content progressively, sending the main content first and loading personalized blocks (shopping cart, user menu) asynchronously:

<?php
// settings.php: enable BigPipe
$settings['big_pipe'] = true;

// BigPipe is enabled by default in Drupal 10+.
// Blocks marked with #cache context 'user' or 'session'
// are loaded via BigPipe after the main content.
<?php
// In a custom block
public function build() {
  return [
    '#markup' => $this->getDynamicContent(),
    '#cache' => [
      'contexts' => ['user'],
      'max-age' => 0, // Never cache personalized content
    ],
  ];
}

Database Optimization

Query Logging with Devel

composer require drupal/devel
drush pm:enable devel
<?php
// Enable query logging in settings.php for development
$settings['devel'] = [
  'query_sort' => 'source',
  'query_count' => 100,
];

Database Tuning

-- MySQL performance tuning for Drupal
SET GLOBAL innodb_buffer_pool_size = 1G;
SET GLOBAL query_cache_size = 256M;
SET GLOBAL tmp_table_size = 64M;
SET GLOBAL max_allowed_packet = 128M;

-- Analyze slow queries
SHOW FULL PROCESSLIST;
SHOW VARIABLES LIKE 'slow_query%';

-- Common Drupal query optimizations
CREATE INDEX node_created_idx ON node_field_data (created DESC);
CREATE INDEX node_title_idx ON node_field_data (title);

Search with Solr/Elasticsearch

# Install search API
composer require drupal/search_api drupal/search_api_solr
drush pm:enable search_api search_api_solr

PHP Tuning

; php.ini for production Drupal
memory_limit = 256M
max_execution_time = 30
max_input_time = 60
upload_max_filesize = 64M
post_max_size = 64M
realpath_cache_size = 256K
realpath_cache_ttl = 600

; Session handling
session.gc_probability = 1
session.gc_divisor = 1000
session.gc_maxlifetime = 200000
session.save_path = /var/lib/php/sessions

; OPcache
opcache.enable = 1
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
opcache.revalidate_freq = 60
opcache.fast_shutdown = 1

Web Server Tuning

Nginx with fastcgi_cache

# nginx.conf: Drupal fastcgi caching
fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=DRUPAL:10m inactive=60m;

server {
  listen 80;
  server_name example.com;
  root /var/www/web;

  # Cache zone for anonymous users
  set $cache_bypass 0;
  if ($cookie_SESS) {
    set $cache_bypass 1;
  }

  location ~ \.php$ {
    fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;

    # Only cache if no session cookie
    fastcgi_cache DRUPAL;
    fastcgi_cache_bypass $cache_bypass;
    fastcgi_no_cache $cache_bypass;
    fastcgi_cache_valid 200 301 302 10m;
    fastcgi_cache_use_stale updating error timeout;
  }
}

Apache mod_cache

# .htaccess: Enable cache
<IfModule mod_cache.c>
  CacheEnable disk /
  CacheHeader on
  CacheDefaultExpire 3600
  CacheMaxExpire 86400
  CacheIgnoreNoLastMod On
  CacheStorePrivate On
</IfModule>

KeepAlive

# httpd.conf: Apache KeepAlive
KeepAlive On
MaxKeepAliveRequests 100
KeepAliveTimeout 5

Image Optimization

# Install ImageAPI Optimize
composer require drupal/imageapi_optimize drupal/imageapi_optimize_webp
drush pm:enable imageapi_optimize imageapi_optimize_webp
# Image style configuration
# Convert to WebP format
binaries:
  webp: /usr/bin/cwebp

# Image style: Large (1200px wide)
name: large_webp
effects:
  scale:
    width: 1200
    height: null
    upscale: false
  convert:
    format: webp
    quality: 85

Enable lazy loading for all images:

<?php
// In your theme or module
function mytheme_preprocess_image(&$variables) {
  $variables['attributes']['loading'] = 'lazy';
}

CSS/JS Aggregation

<?php
// settings.php: enable aggregation
$config['system.performance']['css']['preprocess'] = true;
$config['system.performance']['js']['preprocess'] = true;

// Enable bandwidth optimization
$config['system.performance']['css']['gzip'] = true;
$config['system.performance']['js']['gzip'] = true;

Profiling and Monitoring

WebProfiler

drush pm:enable devel
# Access at /admin/devel -> http://example.com/admin/devel

New Relic

# newrelic.ini
newrelic.appname = "Drupal Production"
newrelic.daemon.logfile = /var/log/newrelic/daemon.log
newrelic.error_collector.enabled = true
newrelic.transaction_tracer.enabled = true
newrelic.transaction_tracer.threshold = "apdex_f"

Load Testing

# Apache Bench (ab)
ab -n 1000 -c 10 http://example.com/

# Siege
siege -c 50 -t 60s http://example.com/

# JMeter (GUI-based)
jmeter -n -t test-plan.jmx -l results.jtl

Common Mistakes

  1. Not enabling page cache for anonymous users: By default, Drupal's page cache is off. Anonymous users get uncached pages, wasting server resources on every request.

  2. Over-caching without invalidation Strategy: If you cache everything with long TTLs, stale content stays visible. Use cache tags and Purge for intelligent invalidation.

  3. Enabling all cache bins without understanding them: Different caches serve different purposes. The render cache helps authenticated users. The page cache only helps anonymous. Configure each based on your traffic patterns.

  4. Not using a CDN for static assets: Serving CSS, JS, and images from the same server as PHP requests increases load. A CDN offloads these files to edge servers closer to visitors.

  5. Skipping database query optimization: A single unoptimized Views query can take seconds. Use Devel to identify slow queries, add database indexes, and optimize Views configurations.

Practice Questions

  1. What is the difference between Page Cache and Dynamic Page Cache, and which users does each serve?
  2. How would you configure BigPipe to load a user's shopping cart asynchronously while the rest of the page loads immediately?
  3. You notice that the site is slow for anonymous users. What are the first three things you check?
  4. Challenge: Set up a complete caching strategy for an e-commerce site built with Drupal. The site has: product listing pages (same for all users), a shopping cart (personalized per user), user account pages (personalized), admin pages (no caching), and static assets (images, CSS, JS). For each type of page, specify: which cache layer applies, the cache TTL, how invalidation works, and whether BigPipe applies.

FAQ

What is the fastest cache for Drupal?

The fastest is Varnish as a reverse proxy, serving cached pages from memory in microseconds. Combined with Drupal's internal page cache and a CDN, you can achieve sub-100ms Time to First Byte for anonymous traffic.

Should I enable CSS and JS aggregation?

Yes, for production. Aggregation combines all CSS into one file and all JS into one file (or a few files), reducing HTTP requests. Always test after enabling, as some modules might not work correctly with aggregation.

How does BigPipe work?

BigPipe sends the main page HTML first, then delivers personalized content (blocks with user-context cache) via JavaScript after the page loads. Users see the main content immediately while personalized sections load progressively.

Do I need Varnish if I have a CDN?

Both serve different purposes. Varnish sits in front of your web server and caches entire pages. A CDN caches static assets at edge locations. For maximum performance, use both: Varnish for HTML pages and CDN for CSS, JS, images, and fonts.

How often should I run load tests?

Run load tests before every major launch, after significant content additions, and after module installs that affect performance. Regular testing (monthly) helps you catch regressions before they affect users.

Mini Project

Goal: Performance-optimize a Drupal site for production traffic.

  1. Enable all relevant cache systems: page cache, dynamic page cache, render cache, views cache, Twig cache
  2. Enable CSS and JS aggregation
  3. Install and configure the Purge module with Varnish (or simulate with internal page cache)
  4. Set up image optimization: install ImageAPI Optimize, create a WebP image style, enable lazy loading
  5. Tune PHP OPcache settings for production
  6. Configure Nginx or Apache caching headers for static files
  7. Run a load test before and after optimization, measuring: requests per second, average response time, error rate, peak memory usage
  8. Document the performance improvement with before/after metrics
  9. Set up New Relic or a similar monitoring tool to track performance over time

What's Next

Now that you understand performance optimization, proceed to backups and maintenance to learn how to keep your Drupal site healthy. Then explore go-live checklist for a complete pre-launch readiness guide.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro