Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Symfony Cache Component with Redis and APCu

By Kokil Thapa | Last reviewed: September 2026

Your Symfony app is fast in dev and sluggish under load. The Symfony Cache Component with Redis and APCu fixes that by splitting cache between a shared Redis store and per-worker APCu memory. Redis holds data every PHP-FPM worker can read. APCu keeps hot keys inside each worker process. Together they cut database hits without stale reads across servers. This guide walks through Symfony 8.1 on PHP 8.5 with Redis 8.10 — the stack I use on production Symfony deployments and sister sites that share a Redis caching pattern for web apps.

What is the Symfony Cache Component with Redis and APCu?

The Symfony Cache Component is a PSR-6 and PSR-16 implementation. It ships adapters for filesystem, Redis, Memcached, APCu, and more. You pick an adapter, assign it to a pool, then inject that pool where your app reads or writes cache.

Redis acts as the shared tier. All app servers and queue workers see the same keys. APCu sits in each PHP-FPM worker as an optional L1 layer. It is process-local and extremely fast for repeated reads within one worker.

On a legal-tech portal or booking system I maintain, Redis holds session fragments, API rate-limit counters, and Doctrine query results. APCu caches config-derived lookup tables that rarely change. That split keeps response times stable during traffic spikes around Dashain booking windows.

Symfony Cache: Redis + APCu TiersSymfony AppControllers, ServicesCache PoolChainAdapterAPCu L1Per PHP-FPM workerRedis L2Shared across serversMiss falls through to DB, API, or computed value
Symfony Cache Component with Redis and APCu — ChainAdapter reads APCu first, then Redis, then the origin source

The component lives in symfony/cache. Symfony 8.1 pulls it in through FrameworkBundle. You rarely touch adapter classes directly. YAML config and autowired pool services do the work. For background on how Symfony wires services, see the Symfony service container guide.

How do you install Redis and APCu for Symfony 8.1?

Before touching framework.yaml, confirm both extensions exist on every server that runs PHP-FPM. Missing APCu on one node causes silent fallback or boot errors depending on your config.

Install PHP extensions on Ubuntu

sudo apt update
sudo apt install php8.5-redis php8.5-apcu
sudo systemctl restart php8.5-fpm

Verify both extensions load:

php -m | grep -E 'redis|apcu'
php -i | grep apc.enabled

APCu must show apc.enabled=1 for web SAPI. CLI often disables APCu by default. That is fine — console commands should use Redis directly, not APCu.

Install and secure Redis 8.10

On production Ubuntu 24.04 hosts I bind Redis to localhost or a private VPC IP. Password auth is mandatory when multiple apps share the host.

sudo apt install redis-server
sudo nano /etc/redis/redis.conf

Set these values in redis.conf:

  • bind 127.0.0.1 ::1 — or your internal network IP
  • requirepass your-strong-password
  • maxmemory 512mb — tune to available RAM
  • maxmemory-policy allkeys-lru — evict least-used keys under pressure

Restart Redis and test:

redis-cli -a your-strong-password ping

Server provisioning steps align with our Symfony deployment on Ubuntu VPS guide. For ongoing server work, Linux system administration covers the same production baseline.

How do you configure Symfony Cache pools with Redis and APCu?

Symfony 8.1 uses config/packages/cache.yaml for pool definitions. The pattern below creates a chain: APCu first, Redis second. Reads hit APCu when the key exists locally. On miss, Symfony checks Redis. On another miss, your code computes the value and writes back through the chain.

Environment variables

Store Redis connection details in .env and override per environment. Never commit production passwords.

REDIS_URL=redis://:your-strong-password@127.0.0.1:6379
APP_CACHE_PREFIX=prod_myapp_

Multi-environment setups benefit from the patterns in Symfony environment config for multi-environment apps.

framework.yaml cache configuration

framework:
    cache:
        prefix_seed: '%env(APP_CACHE_PREFIX)%'

        app: cache.adapter.redis
        default_redis_provider: '%env(REDIS_URL)%'

        pools:
            cache.app:
                adapter: cache.adapter.apcu
                default_lifetime: 3600

            cache.redis:
                adapter: cache.adapter.redis
                provider: '%env(REDIS_URL)%'
                default_lifetime: 3600

            cache.chain:
                adapters:
                    - cache.adapter.apcu
                    - cache.adapter.redis
                default_lifetime: 3600

            doctrine.result_cache_pool:
                adapter: cache.chain
                default_lifetime: 86400

            doctrine.system_cache_pool:
                adapter: cache.adapter.redis
                default_lifetime: 604800

Doctrine integration in config/packages/doctrine.yaml:

doctrine:
    orm:
        query_cache_driver:
            type: pool
            pool: doctrine.system_cache_pool
        result_cache_driver:
            type: pool
            pool: doctrine.result_cache_pool

Enable result caching in a repository query:

$query = $this->createQueryBuilder('b')
    ->where('b.status = :status')
    ->setParameter('status', 'published')
    ->getQuery()
    ->enableResultCache(3600, 'blog_published_list');

Inject a custom pool in a service

services:
    App\Service\RateLimitService:
        arguments:
            $cache: '@cache.redis'
namespace App\Service;

use Psr\Cache\CacheItemPoolInterface;

class RateLimitService
{
    public function __construct(
        private CacheItemPoolInterface $cache,
    ) {}

    public function isAllowed(string $ip, int $limit = 60): bool
    {
        $key = 'rate_' . md5($ip);
        $item = $this->cache->getItem($key);
        $count = $item->isHit() ? (int) $item->get() : 0;

        if ($count >= $limit) {
            return false;
        }

        $item->set($count + 1);
        $item->expiresAfter(60);
        $this->cache->save($item);

        return true;
    }
}

Rate limiting ties directly to API rate limiting and abuse prevention. Use Redis-only pools for counters that must stay consistent across workers.

Cache Read Flow: Hit or MissRequestAPCuL1 checkmissRedisL2 checkmissOriginHIT at any tier — return value, populate faster tiers on Redis hitAPCu HIT~0.01 ms — no networkRedis HIT~0.5 ms — fills APCuOrigin MISS — compute, write Redis + APCuSet TTL on every stored item
Read path for Symfony Cache Component with Redis and APCu — each miss falls through to the next tier

When should you use Redis vs APCu in Symfony?

Both adapters implement the same PSR-6 interface. They behave very differently under load, across servers, and during deploys. Pick the tier based on consistency needs, not raw speed alone.

CriteriaAPCuRedis
ScopeSingle PHP-FPM worker processShared across all workers and servers
Typical latencySub-millisecond in-processSub-millisecond LAN; 1–5 ms over network
Survives deployNo — cleared on worker restartYes — persists until TTL or flush
Best forConfig snapshots, enum maps, hot read-mostly keysSessions, rate limits, Doctrine cache, tag invalidation
Memory limitShared with PHP worker (apc.shm_size)Dedicated Redis maxmemory with eviction policy
Tag-aware invalidationNot reliable across workersYes with RedisTagAwareAdapter
Multi-server consistencyNone — each server differsFull — single source of truth

Use APCu only for data that can tolerate per-worker staleness for a few seconds. Use Redis when any worker must see the same value immediately after a write or invalidation. For tag-based cache busting, compare cache tags with Redis vs Memcached — Symfony's Redis tag adapter follows similar trade-offs.

On Adventure Third Pole Trek, a Laravel + Livewire booking app on similar infrastructure, Redis holds availability slots while APCu-style opcache covers compiled PHP. The Symfony equivalent uses explicit pool config instead of Laravel's Cache::remember() facade, but the tier logic is the same.

How do you handle cache invalidation and tags in production?

Stale cache hurts more than no cache. Symfony gives you TTL expiry, explicit deletes, and tag-based invalidation for grouped busting. Plan invalidation before you cache — not after users report wrong prices or outdated legal document lists.

TTL as your first line of defence

Every cached item needs expiresAfter() or a pool-level default_lifetime. Short TTLs on frequently changing data beat complex invalidation for many CRUD apps. Court fee tables on a legal portal might use 24-hour TTL. Live booking counts might use 60 seconds.

Tag-aware invalidation with Redis

Symfony's RedisTagAwareAdapter tracks tag-to-item relationships in Redis. Bust all items tagged blog when an editor publishes a post.

framework:
    cache:
        pools:
            cache.tagged:
                adapter: cache.adapter.redis_tag_aware
                provider: '%env(REDIS_URL)%'
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface;

public function getPublishedPosts(TagAwareCacheInterface $cache): array
{
    return $cache->get('posts_published', function (ItemInterface $item) {
        $item->tag(['blog', 'posts']);
        $item->expiresAfter(3600);

        return $this->postRepository->findPublished();
    });
}

public function invalidateBlog(TagAwareCacheInterface $cache): void
{
    $cache->invalidateTags(['blog']);
}

Do not put tag-aware pools behind APCu in a chain. APCu cannot propagate tag invalidation to other workers. Keep tagged data on Redis only.

Manual invalidation on entity changes

Doctrine lifecycle callbacks or event subscribers can clear specific keys when entities change:

public function postUpdate(BlogPost $post, PostUpdateEventArgs $event): void
{
    $this->cache->deleteItem('blog_post_' . $post->getId());
    $this->taggedCache->invalidateTags(['blog']);
}

Event-driven patterns mirror Symfony Event Dispatcher real-world patterns. Pair cache clears with Symfony Messenger when invalidation triggers heavy rebuild work.

Choose the Right Cache PoolAPCu OnlyStatic lookupsSingle-server appsDev/staging speedRedis OnlyMulti-server prodTags + sessionsShared countersChain BothHigh read trafficDoctrine resultsAPI responsesAvoid APCu for tagged or write-heavy dataProduction default: Chain (APCu + Redis) for readsRedis-only pools for tags, sessions, rate limits
Pool selection matrix for Symfony Cache Component with Redis and APCu in production

What production tuning and debugging steps prevent cache failures?

Cache bugs show up as intermittent wrong data, not stack traces. A user in Kathmandu sees updated fees. A user on another FPM worker still sees yesterday's APCu copy. These steps catch that class of problem before launch.

APCu memory sizing

Default apc.shm_size is often 32M — too small for busy apps. Raise it in a dedicated ini file:

apc.enabled=1
apc.shm_size=128M
apc.entries_hint=4096
apc.ttl=7200

Monitor usage with apcu_cache_info() or the APCu status page in a protected admin route. Never expose APCu stats publicly.

Redis persistence and eviction

Cache data is disposable. Many teams run Redis without AOF for pure cache workloads. If you rely on Redis for sessions too, review Redis persistence and clustering before disabling durability features.

Clear cache safely during deploy

Symfony's cache:clear rebuilds the container and warms pools. On Deployer 7 releases I run:

  1. php bin/console cache:clear --env=prod
  2. php bin/console cache:warmup --env=prod
  3. Reload PHP-FPM to reset APCu and opcache

Skipping the FPM reload leaves stale APCu entries from the previous release. I have debugged exactly that on shared EC2 hosts running multiple legal-tech sister sites.

Validate config with the JSON formatter

When debugging serialized cache payloads, paste Redis values into the JSON formatter tool to inspect structure. Symfony serializes PHP values by default unless you configure a custom marshaller.

HTTP cache vs application cache

Do not confuse Symfony HttpCache with the Cache Component. HttpCache sits in front of the kernel for full-page responses. Application pools cache fragments, queries, and computed objects. Speed optimization work often combines both layers plus CDN edge caching.

Monitoring checklist

  • Track Redis used_memory, evicted keys, and connected clients
  • Alert when evictions spike — your working set exceeds maxmemory
  • Log cache miss ratio for expensive Doctrine queries
  • Compare response times before and after enabling the chain adapter
  • Run load tests with multiple FPM workers to expose APCu inconsistency bugs

Official references: the Symfony Cache Component documentation, the Redis documentation, and the PHP APCu manual cover adapter options and extension settings in full.

Deploy: Avoid Stale APCuGit pushNew releasecache:clearSymlink swapCommon mistake: skip FPM reloadOld APCu keys survive in running workersCorrect: reload php8.5-fpmAPCu cleared — all workers read fresh RedisZero-downtime deploy + warm cache pools
Deploy workflow for Symfony Cache Component with Redis and APCu — always reload PHP-FPM after symlink swap

For enterprise Symfony builds with strict SLAs, enterprise application development and testing and optimization services cover load testing and cache strategy reviews. Clean architecture helps isolate cache behind interfaces — see hexagonal architecture with Symfony.

If you are choosing a framework first, the Symfony vs Laravel comparison covers cache ergonomics alongside other factors. Nepal Gift Card runs on Laravel with Redis caching at a similar tier. The adapter names differ, but production tuning goals match.

HTTP APIs cached at the service layer pair well with Symfony API Platform. Serializer output cached in Redis avoids repeated DB joins on list endpoints. That pattern reduced p95 latency on a directory project without touching front-end assets.

Key Takeaways

  • Stack APCu as L1 and Redis as L2 with ChainAdapter for read-heavy Symfony 8.1 pools on PHP 8.5.
  • Keep tag-aware, session, and rate-limit pools on Redis only — never behind APCu.
  • Set explicit TTLs on every pool and item; treat invalidation as a design requirement.
  • Reload PHP-FPM after deploy so APCu does not serve keys from the previous release.
  • Size apc.shm_size and Redis maxmemory before launch, then monitor evictions under real load.
  • Use official Symfony, Redis, and APCu docs when adapter options or extension settings change between minor releases.

People Also Ask

Does Symfony 8.1 require APCu for caching?

No. Symfony runs fine with filesystem or Redis adapters alone. APCu is optional. It adds an in-process speed layer on single-server or multi-worker hosts where repeated reads dominate. Skip APCu on CLI-only workers or when every byte must be consistent across processes instantly.

Can you use Redis and APCu together in the same cache pool?

Yes. Configure a chain pool with APCu first and Redis second. Symfony reads APCu, falls through to Redis on miss, then writes back through the chain. Do not chain tag-aware Redis adapters behind APCu — tag invalidation will not reach other workers.

How do you clear Symfony cache without breaking production?

Run cache:clear and cache:warmup in the new release directory before swapping the symlink. Reload PHP-FPM to flush APCu. Use redis-cli FLUSHDB only when you accept losing all Redis keys on that database — never flush shared Redis instances used by multiple apps.

Is APCu the same as OPcache in Symfony?

No. OPcache stores compiled PHP bytecode. APCu stores application data your code puts there via the Cache Component. Both live in shared memory, but they serve different layers. Enable both on production PHP-FPM hosts for maximum benefit.

Ship faster Symfony apps with the right cache tiers

Symfony Cache Component with Redis and APCu gives you a practical two-tier setup without custom infrastructure. Redis shares state across workers. APCu shaves milliseconds off hot reads inside each process. Configure pools deliberately, invalidate on write, and reload FPM on every deploy. That is the baseline I apply on production Symfony systems and on related web development projects from Kathmandu and remote clients worldwide.

Need help auditing cache config on an existing Symfony app or planning tiers for a new build? Review the enterprise Symfony development service or contact us for a production cache review. For background on Kokil's Symfony and DevOps work, see about me and the full project portfolio.

Frequently Asked Questions

Symfony’s Cache Component is a PSR-6 and PSR-16 cache layer in symfony/cache, wired through FrameworkBundle. Redis is the shared tier every PHP-FPM worker and server reads. APCu is an optional L1 inside each worker for hot keys. ChainAdapter checks APCu first, then Redis, cutting database hits without stale reads across servers.

No. Symfony 8.1 runs fine with filesystem or Redis adapters alone. APCu is optional and adds in-process speed where repeated reads dominate. Skip it on CLI-only workers or when every worker must see identical values immediately after a write.

Yes. Define a chain pool with APCu first and Redis second. Reads hit APCu, fall through to Redis on miss, then write back through the chain. Never put RedisTagAwareAdapter behind APCu — tag invalidation will not reach other workers.

On Ubuntu, run apt update, then install php8.5-redis and php8.5-apcu, and restart php8.5-fpm. Verify with php -m for both extensions and php -i to confirm apc.enabled=1 for the web SAPI. CLI often disables APCu by default, which is acceptable because console commands should use Redis directly. Missing APCu on one node in a cluster can cause silent fallback or boot errors depending on your pool configuration.

Store REDIS_URL and APP_CACHE_PREFIX in .env, then define pools in config/packages/cache.yaml under framework.cache. Set prefix_seed from APP_CACHE_PREFIX, default_redis_provider from REDIS_URL, and create separate pools for APCu, Redis, and a chain combining both. Wire doctrine.result_cache_pool to the chain adapter and doctrine.system_cache_pool to Redis in doctrine.yaml. Inject cache.redis into services like rate limiters that need cross-worker consistency.

Use Redis when any worker or server must see the same value immediately after a write, invalidation, or deploy — sessions, rate-limit counters, Doctrine caches, and tag-based busting all belong here. Use APCu only for read-mostly data that tolerates per-worker staleness for a few seconds, such as config-derived lookup tables or enum maps. Redis survives deploys and supports RedisTagAwareAdapter; APCu is cleared on worker restart and cannot propagate tag invalidation across processes.

Install redis-server, then edit redis.conf on Ubuntu 24.04 hosts. Bind to 127.0.0.1, ::1, or a private VPC IP — never expose Redis publicly. Set requirepass to a strong password and reference it in REDIS_URL as redis://:password@127.0.0.1:6379. Configure maxmemory such as 512mb and maxmemory-policy allkeys-lru so Redis evicts least-used keys under pressure instead of crashing. Restart Redis and confirm with redis-cli -a your-password ping.

Use RedisTagAwareAdapter on a dedicated Redis-only pool, never chained behind APCu. Tag items during cache writes with expiresAfter TTLs, then call invalidateTags when content changes — for example busting all items tagged blog after a post publishes. Pair manual key deletes in Doctrine lifecycle callbacks with tag invalidation for grouped busting. Plan invalidation before caching: stale legal fee tables or booking counts hurt more than no cache. Short TTLs on frequently changing data often beat complex invalidation for simple CRUD apps.

In doctrine.yaml, point query_cache_driver to doctrine.system_cache_pool using the Redis adapter, and result_cache_driver to doctrine.result_cache_pool using the chain adapter. Set pool default_lifetime values such as 86400 seconds for results and 604800 for system metadata. Enable result caching per query with enableResultCache and a stable cache key. This keeps repeated repository reads off the database while Redis ensures consistency across workers for query metadata.

APCu lives inside each PHP-FPM worker process and does not survive worker restarts cleanly across symlink swaps. After Deployer 7 releases, running cache:clear and cache:warmup in the new release directory rebuilds Symfony pools, but skipping an FPM reload leaves stale APCu entries from the previous release. I have debugged exactly this on shared EC2 hosts running multiple legal-tech sister sites — one worker serves fresh Redis data while another still reads old APCu keys. Always reload PHP-FPM after the symlink swap.

Default apc.shm_size is often 32M, which is too small for busy Symfony apps. Raise it in a dedicated ini file — apc.enabled=1, apc.shm_size=128M, apc.entries_hint=4096, apc.ttl=7200 — then restart PHP-FPM. Monitor usage with apcu_cache_info or a protected admin route showing APCu status; never expose APCu stats publicly. Under-provisioned shared memory causes evictions and unexpected misses that push load back to Redis or the database during traffic spikes.

Run php bin/console cache:clear --env=prod and cache:warmup --env=prod inside the new release directory before swapping the Deployer 7 symlink. Reload PHP-FPM afterward to flush APCu and opcache. Avoid redis-cli FLUSHDB unless you accept losing every Redis key on that database — sessions, rate limits, and Doctrine caches included. This workflow rebuilds the container, warms pools, and ensures no worker serves keys compiled under the previous release.

Use Redis-only pools for rate limiting, sessions, and any counter that must stay consistent across all PHP-FPM workers and app servers. The article’s RateLimitService example injects cache.redis directly because APCu is process-local — one worker could allow requests while another blocks the same IP. Redis holds session fragments and API rate-limit counters on production legal-tech and booking systems. Reserve the APCu-plus-Redis chain for read-heavy, eventually-consistent data like Doctrine result caches or config snapshots.

Symfony HttpCache sits in front of the kernel and caches full HTTP responses at the edge of your application. The Cache Component pools cache fragments, Doctrine query results, computed objects, and service-layer data inside Redis and APCu. They solve different problems and are often combined — HttpCache for whole pages, Redis chain pools for expensive backend reads, plus CDN edge caching for static assets. Do not configure one expecting it to replace the other; speed work typically layers all three.

Track Redis used_memory, evicted_keys, and connected_clients — alert when evictions spike because your working set exceeds maxmemory. Log cache miss ratios on expensive Doctrine queries and compare response times before and after enabling ChainAdapter. Run load tests with multiple FPM workers to expose APCu inconsistency bugs where one worker serves stale L1 data. Validate serialized payloads with Symfony’s JSON formatter when debugging Redis values. Size apc.shm_size and Redis maxmemory before launch, then re-check under real load during booking or campaign traffic peaks.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: