---
title: "Header Expires per file htaccess"
description: "Caching del browser via .htaccess nel 2026: perché Cache-Control ha superato Expires, e come il fingerprinting degli asset cambia tutto rispetto alla regola \"1 mese per tipo\"."
canonical: "https://www.mauriziofonte.it/blog/post/header-expires-htaccess.html"
author: "Maurizio Fonte"
date: "2026-04-01T12:26:59+02:00"
modified: "2026-06-15T18:01:16+02:00"
section: "Performance"
tags: ["Performance", "DevOps"]
license: "Riuso e citazione consentiti con attribuzione e link alla fonte"
---

[Performance](https://www.mauriziofonte.it/blog/cat/performance.html) [DevOps](https://www.mauriziofonte.it/blog/cat/devops.html)

# [Header Expires per file htaccess](https://www.mauriziofonte.it/blog/post/header-expires-htaccess.html)

Mercoledì 1 Aprile 2026, alle 12:26 ![Header Expires per file htaccess](https://www.mauriziofonte.it/image/000400200-header-expires-htaccess.jpg)

In un progetto di migrazione infrastrutturale per una PMI ho ricevuto una segnalazione che si ripeteva a ogni rilascio: dopo ogni aggiornamento del sito, una parte degli utenti vedeva il layout rotto, con il CSS nuovo applicato a una struttura HTML vecchia o viceversa, e la cosa si risolveva "da sola" solo dopo giorni. La causa era nel file .htaccess: conteneva esattamente il blocco `mod_expires` che si trova in mille tutorial, quello che imposta un mese di cache per CSS e JavaScript per tipo MIME. Funzionava benissimo per la velocità, e altrettanto bene per servire agli utenti la versione vecchia degli asset per un mese dopo ogni deploy.

Quel caso è il motivo per cui vale la pena rivedere da capo come si imposta il caching del browser via .htaccess nel 2026, perché la tecnica di base è ancora valida e utile, ma la ricetta classica nasconde un difetto strutturale che non si vede finché non rilasci un aggiornamento. E la soluzione, controintuitivamente, non sta nel regolare meglio la durata della cache: sta nel cambiare il nome dei file.

## TL;DR

> - **Header moderno:** usa `Cache-Control`, non `Expires` (quando ci sono entrambi vince Cache-Control); `mod_expires` lo emette già.
> - **Il vero fix non è la durata, è il nome del file:** fai il fingerprinting degli asset (`app.4f3a9c1.js`) e dai loro `Cache-Control: max-age=31536000, immutable`.
> - **HTML:** mai cache lunga (`max-age=0`, sempre fresco); asset fingerprinted: un anno + `immutable`.
> - **Risorse non fingerprintabili (es. immagini caricate):** revalidation con `ETag`/`Last-Modified` (risposta 304).
> - **Verifica:** `curl -I` sugli asset e l'audit "efficient cache policy" di Lighthouse/PageSpeed.

## Cache-Control ha superato Expires: cosa cambia davvero

Partiamo dal nome dell'articolo, perché è già parte del problema. L'header `Expires` indica al browser una data assoluta oltre la quale una risorsa è considerata scaduta. È il meccanismo storico, ed è quello che il modulo `mod_expires` di Apache configura. Il punto è che oggi lo standard di riferimento per il caching HTTP è l'header `Cache-Control`, che esprime la stessa intenzione in modo relativo e molto più espressivo: `max-age=2592000` dice "questa risorsa è valida per 2.592.000 secondi (un mese) dal momento in cui l'hai ricevuta", senza dipendere dall'orologio del client né da una data fissa. Quando entrambi gli header sono presenti, i browser danno la precedenza a `Cache-Control`, quindi `Expires` è di fatto un fallback per client antichi. La buona notizia è che `mod_expires`, quando lo configuri, emette automaticamente anche l'header `Cache-Control: max-age` corrispondente, come spiega la [documentazione ufficiale di mod_expires](https://httpd.apache.org/docs/current/mod/mod_expires.html). Quindi non stai sbagliando a usarlo, ma è bene sapere che lo strumento moderno che vuoi davvero controllare è `Cache-Control`, e che ha direttive che `Expires` non può esprimere.

Le direttive che contano, e che il blocco del 2017 non usava perché allora non erano diffuse, sono tre. `immutable` dice al browser che la risorsa non cambierà mai per tutta la durata della sua validità, così che non tenti nemmeno una richiesta di revalidation quando l'utente ricarica la pagina: un guadagno reale di richieste risparmiate. `stale-while-revalidate` permette al browser di servire una risorsa scaduta mentre ne controlla in background una versione aggiornata, eliminando l'attesa percepita. E `public` o `private` distinguono ciò che può essere messo in cache da proxy e CDN condivisi da ciò che è specifico del singolo utente. Tutte e tre sono documentate nella pagina di riferimento di [MDN su Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control), ed è lì che vive oggi la logica del caching, non nell'header `Expires`.

## Il vero problema non è la durata, è il nome del file

Ora il cuore della questione, quello che trasformava una buona configurazione in un generatore di disservizi. La regola "CSS e JS scadono dopo un mese" contiene una contraddizione insanabile se i file hanno nomi fissi come `style.css` o `app.js`. Da un lato vuoi una cache lunga, perché quegli asset cambiano di rado e ricaricarli a ogni visita è uno spreco. Dall'altro, quando finalmente li aggiorni con un deploy, vuoi che il browser scarichi subito la versione nuova, non fra un mese. Con un nome di file fisso e una cache di un mese, queste due esigenze sono in conflitto diretto, e il classico `mod_expires` per tipo MIME risolve il conflitto nel modo sbagliato: privilegia la durata e sacrifica la freschezza, lasciando gli utenti con asset obsoleti dopo ogni rilascio.

La soluzione è il fingerprinting (o cache busting): far sì che il nome del file cambi quando il suo contenuto cambia. Invece di servire `app.js`, si serve `app.4f3a9c1.js`, dove quella stringa è un hash del contenuto. Quando modifichi il file, l'hash cambia, quindi cambia il nome, quindi per il browser è una risorsa nuova che non ha mai visto e che scarica immediatamente, mentre la vecchia resta in cache inutilizzata e prima o poi viene scartata. Questo ribalta completamente la logica: a un asset con nome fingerprinted puoi dare la cache più aggressiva possibile, un anno e `immutable`, proprio perché quel preciso file non cambierà mai (se cambiasse, avrebbe un altro nome). Ed è esattamente ciò che fanno gli asset bundler moderni, da Vite a Webpack agli strumenti integrati nei framework: generano nomi con hash di default. La domanda giusta, quindi, non è "quanto faccio durare la cache dei CSS", ma "i miei asset hanno un nome fingerprinted?". Se sì, la durata diventa banale; se no, nessuna durata è quella giusta.

Se gestisci un sito dove i deploy lasciano gli utenti con risorse vecchie, o dove al contrario la cache è troppo timida e ogni pagina ricarica tutto, è il tipo di problema su cui intervengo regolarmente: nel mio [profilo professionale](https://www.mauriziofonte.it/pages/about) trovi l'esperienza concreta su tuning di server web, performance e pipeline di asset per applicativi in produzione.

## Una configurazione .htaccess corretta per il 2026

Mettendo insieme i pezzi, ecco come riscrivo il blocco di caching tenendo conto del fingerprinting. La logica è a due velocità: l'HTML non si mette mai in cache a lungo (è il punto di ingresso che deve sempre riflettere l'ultima versione e referenziare i nomi fingerprinted aggiornati), mentre gli asset statici fingerprinted ricevono la cache massima con `immutable`.

```apache
<IfModule mod_expires.c>
  ExpiresActive On

  # HTML: mai cache lunga, e' il punto d'ingresso che deve restare fresco
  ExpiresByType text/html                 "access plus 0 seconds"

  # Asset statici fingerprinted: cache massima, immutabile
  ExpiresByType text/css                  "access plus 1 year"
  ExpiresByType application/javascript    "access plus 1 year"
  ExpiresByType image/svg+xml             "access plus 1 year"
  ExpiresByType font/woff2                "access plus 1 year"

  # Immagini di contenuto (non fingerprinted): durata media
  ExpiresByType image/webp                "access plus 1 month"
  ExpiresByType image/avif                "access plus 1 month"
</IfModule>

<IfModule mod_headers.c>
  # Aggiunge immutable agli asset con hash nel nome (es. app.4f3a9c1.js)
  <FilesMatch "\.[0-9a-f]{8,}\.(css|js|woff2|svg)$">
    Header set Cache-Control "public, max-age=31536000, immutable"
  </FilesMatch>
</IfModule>
```

Noterai alcune scelte rispetto al blocco del 2017. I tipi MIME obsoleti sono spariti: `application/x-font-woff` ha lasciato il posto a `font/woff2`, che è il formato di font del web moderno; `text/cache-manifest` (l'appcache) è una tecnologia deprecata da anni e non ha più senso configurarla; i formati immagine includono ora `webp` e `avif`, che sono quelli che si servono oggi. E soprattutto, la cache lunga con `immutable` è applicata in modo mirato ai soli file che hanno un hash nel nome, tramite `FilesMatch`, non a tutti i CSS e JS indistintamente. Questo è il dettaglio che rende la configurazione insieme aggressiva sulla performance e sicura sui rilasci: solo ciò che è davvero immutabile riceve il trattamento da immutabile.

Resta una categoria di risorse che non si può fingerprintare: i contenuti dinamici o caricati dagli utenti, come le immagini di un catalogo che possono cambiare mantenendo lo stesso URL. Per questi non si usa la cache immutabile, ma la revalidation: gli header `ETag` (un'impronta del contenuto) e `Last-Modified` (la data di ultima modifica) permettono al browser di chiedere al server "è cambiato rispetto a quello che ho?" inviando un valore di confronto. Se il contenuto è invariato, il server risponde con uno stato 304 Not Modified e un corpo vuoto, e il browser riusa la sua copia: si paga il costo di un round-trip di rete, ma non quello di ritrasferire la risorsa. È il compromesso giusto quando non puoi permetterti la cache lunga perché il contenuto può cambiare, ma vuoi comunque evitare di riscaricare ciò che non è cambiato. La regola pratica è netta: cache lunga e immutabile per gli asset fingerprinted, revalidation con ETag per i contenuti dinamici a URL stabile, nessuna cache per l'HTML.

## Come verificare e misurare che il caching funzioni

Una configurazione di caching va sempre verificata, perché il modo più silenzioso in cui fallisce è "sembra impostata ma non ha effetto". Il primo controllo è diretto: si ispezionano gli header di risposta di una risorsa. Dagli strumenti per sviluppatori del browser, nella scheda di rete, si guarda l'header `Cache-Control` di un CSS o di un'immagine e si verifica che sia quello atteso; ricaricando la pagina, una risorsa cachata correttamente compare come servita dalla cache (memory o disk cache) e non come una nuova richiesta con stato 200. Lo stesso si fa da riga di comando con `curl -I` sull'URL della risorsa, che mostra gli header senza scaricare il corpo: è il modo più rapido per confermare che il server stia davvero emettendo `Cache-Control: public, max-age=31536000, immutable` su un asset fingerprinted e `max-age=0` sull'HTML.

C'è poi un controllo che conta come misura d'impatto: gli strumenti di analisi delle performance come Lighthouse e PageSpeed Insights hanno un audit specifico, "serve static assets with an efficient cache policy", che segnala esattamente le risorse con una cache troppo corta o assente. Vederlo passare al verde è la conferma che la configurazione è efficace agli occhi degli stessi sistemi che Google usa per valutare la velocità del sito, quella velocità che, come è noto da tempo, è un fattore di ranking reale.

Vale infine la pena sapere che il caching è solo metà della delivery efficiente degli asset: l'altra metà è la compressione. Servire CSS e JavaScript compressi con Brotli (o in subordine gzip) riduce drasticamente il peso trasferito, e si configura nello stesso .htaccess con `mod_deflate` o `mod_brotli`. Caching e compressione lavorano insieme, la prima evita di ritrasferire ciò che il client ha già, la seconda riduce il costo del primo trasferimento, e una delivery curata le imposta entrambe. C'è anche un dettaglio da non dimenticare quando si comprime e si serve contenuto negoziato: l'header `Vary` deve riflettere le dimensioni su cui la risposta varia (tipicamente `Accept-Encoding`), altrimenti una cache condivisa potrebbe servire una versione compressa a un client che non la supporta.

## Apache, Nginx, e il livello che spesso conta di più

Due note di contesto, perché il .htaccess non è l'intera storia. La prima: il file .htaccess esiste solo nel mondo Apache, e molti progetti sono migrati o nascono su Nginx, dove non esiste un file equivalente per-directory e la stessa logica di caching si esprime nella configurazione del server con direttive `expires` e `add_header Cache-Control` dentro i blocchi `location`. Il principio resta identico (HTML fresco, asset fingerprinted immutabili), cambia solo la sintassi; se il tuo stack è su Nginx, l'impostazione va fatta lì, ed è uno dei tasselli che curo quando configuro un server da zero, come nel percorso che descrivo nell'articolo su [Let's Encrypt e Nginx con automazione multi-dominio](https://www.mauriziofonte.it/blog/post/lets-encrypt-certbot-nginx-automazione-multi-dominio-linux.html).

La seconda, ed è quella che oggi pesa di più: davanti al server applicativo c'è quasi sempre un livello di edge cache, una CDN. E la CDN obbedisce agli stessi header `Cache-Control` che imposti per il browser, quindi una configurazione fatta bene non accelera solo il singolo visitatore, ma istruisce la CDN su cosa può servire dalla sua cache distribuita senza disturbare il tuo server, riducendo carico e latenza per tutti. Questo intreccio fra caching del browser, caching della CDN e caching applicativo è un sistema a più livelli che va progettato in modo coerente, perché un header sbagliato a un livello vanifica il lavoro degli altri; l'ho affrontato in dettaglio dal lato applicativo nell'articolo sul [caching multilivello in Laravel per siti ad alto traffico](https://www.mauriziofonte.it/blog/post/caching-multilivello-laravel-strategie-alto-traffico.html).

Resta valido, infine, un avvertimento pratico che vale sempre la pena ribadire: il file .htaccess è potente e fragile insieme. Un errore di battitura in una direttiva, un modulo non attivo, una riga fuori posto, e Apache risponde con un errore 500 che mette offline l'intero sito. Prima di toccarlo si fa sempre un backup, si verifica con gli strumenti per sviluppatori del browser che gli header di risposta delle risorse siano effettivamente quelli attesi, e si tiene a mente che su CMS come WordPress un plugin o un altro frammento di configurazione può sovrascrivere il comportamento che hai impostato.

Impostare il caching del browser via .htaccess, nel 2026, non è più una questione di copiare un blocco `ExpiresByType` e scegliere una durata. È capire che `Cache-Control` con `immutable` ha superato `Expires`, che il vero abilitatore di una cache aggressiva è il fingerprinting degli asset e non la durata in sé, e che lo stesso header governa browser e CDN insieme. La ricetta da un mese per tipo MIME che gira da anni ti dà velocità al prezzo di servire versioni vecchie dopo ogni rilascio, esattamente il disservizio da cui sono partito. Se vuoi una configurazione di caching che sia veloce senza tradirti al primo deploy, [contattami per un confronto diretto](https://www.mauriziofonte.it/pages/contattami): di solito basta correggere l'impostazione una volta, agganciandola al modo in cui costruisci gli asset, perché smetta di essere un problema a ogni aggiornamento.

Dall'articolo al lavoro vero

Se questo server lo devi tenere in piedi tu

Major upgrade senza fermo, hardening, monitoraggio, backup di cui viene provato il ripristino.

[Vedi come lavoro sui server](https://www.mauriziofonte.it/servizi/consulenza-it-devops.html) Ultima modifica: Lunedì 15 Giugno 2026, alle 18:01 [« Articolo precedenteSandboxing di agent LLM che eseguono codice arbitrario: container effimeri, seccomp, capability dropping1 Aprile 2026](https://www.mauriziofonte.it/blog/post/sandboxing-agent-llm-codice-arbitrario-container-effimeri-seccomp.html)[Articolo successivo »Vocabolario AI 2026: i concetti tecnici che un decisore IT deve distinguere davvero2 Aprile 2026](https://www.mauriziofonte.it/blog/post/vocabolario-ai-2026-concetti-tecnici-decisori-it-pmi.html)

### Articoli correlati a Header Expires per file htaccess

- [Sicurezza upload immagini in Laravel 12: perché la regola image esclude gli SVG e come validare i file in modo sicuro](https://www.mauriziofonte.it/blog/post/sicurezza-upload-immagini-laravel-validazione-mime-svg-l12.html)

  La PR 54331 di Laravel 12 gennaio 2025 ha escluso gli SVG dalla regola image di default un breaking change giustificato dal fatto che oltre 500 · 27 Febbraio 2025

  ![Sicurezza upload immagini in Laravel 12: perché la regola image esclude gli SVG e come validare i file in modo sicuro](https://www.mauriziofonte.it/image/001450070-sicurezza-upload-immagini-laravel-validazione-mime-svg-l12.jpg)
- [Google PageSpeed insights: il metro di paragone tra un sito web “fatto bene” e uno “fatto male”](https://www.mauriziofonte.it/blog/post/perche-google-pagespeed-insights.html)

  7 Giugno 2023

  ![Google PageSpeed insights: il metro di paragone tra un sito web “fatto bene” e uno “fatto male”](https://www.mauriziofonte.it/image/000640070-perche-google-pagespeed-insights.jpg)
- [GraphQL con Laravel Lighthouse: quando conviene rispetto a REST e come implementarlo](https://www.mauriziofonte.it/blog/post/graphql-laravel-lighthouse-quando-conviene-implementazione.html)

  Ho valutato GraphQL per il refactoring dell'API di un'applicazione mobile Laravel usata da 10.000 utenti. La promessa del 'un endpoint per tutto' · 4 Dicembre 2025

  ![GraphQL con Laravel Lighthouse: quando conviene rispetto a REST e come implementarlo](https://www.mauriziofonte.it/image/003450070-graphql-laravel-lighthouse-quando-conviene-implementazione.jpg)
- [Caching strategico con Redis in Laravel: il caso dei cataloghi e-commerce](https://www.mauriziofonte.it/blog/post/caching-strategico-redis-laravel-12-cataloghi-ecommerce.html)

  Un catalogo e commerce sotto carico vive o muore sul caching, ma cachare male serve dati sbagliati. Strategia con Redis in Laravel: cosa conviene · 3 Settembre 2026

  ![Caching strategico con Redis in Laravel: il caso dei cataloghi e-commerce](https://www.mauriziofonte.it/image/005220070-caching-strategico-redis-laravel-12-cataloghi-ecommerce.jpg)
- [Symfony 7.2: le novità degli attributes e del DI container che semplificano tutto](https://www.mauriziofonte.it/blog/post/symfony-72-novita-attributes-dependency-injection-container.html)

  Symfony 7.2 porta un utilizzo ancora più estensivo degli attributes PHP 8 che elimina gran parte della configurazione YAML che ho sempre trovato · 11 Novembre 2025

  ![Symfony 7.2: le novità degli attributes e del DI container che semplificano tutto](https://www.mauriziofonte.it/image/003530070-symfony-72-novita-attributes-dependency-injection-container.jpg)

---

**Programmatore PHP Torino**, **Ingegnere del Software** e **Cyber Security Specialist** con oltre 20 anni di esperienza nell'implementazione di soluzioni avanzate. Eseguo **porting di vecchi ambienti PHP** verso release più moderne (7.x o 8.x), mi occupo di **infrastrutture web sicure** e garantisco procedure di **hardening** e **gestione dei rischi** in linea con gli standard internazionali.  
Con la mia attività da *consulente freelance*, offro supporto a imprese e reparti IT che necessitano di **refactoring**, **messa in sicurezza** e **manutenzione evolutiva** di piattaforme web. Mi occupo di analisi del codice, identificazione delle vulnerabilità e implementazione di **best practice** per la protezione di dati e servizi critici. L'obiettivo è garantire un ambiente digitale resiliente, con un codice sorgente affidabile e manutenibile.

---

## Autore

**Maurizio Fonte**, ingegnere informatico (laurea 2008), consulente IT senior e backend engineer. Nel settore IT dal 2005, libero professionista con P.IVA dal 2014. Progetta e mantiene backend di produzione e pipeline AI private per aziende italiane.

Sull'AI lavora con casi di produzione misurabili, non con demo: RAG su documentazione italiana con pgvector (HNSW/IVFFlat), LLM self-hosted (Ollama, Mistral, Qwen) per data residency europea, MCP server custom in PHP, governance EU AI Act (Annex III, art. 16, FRIA) e sicurezza degli agent (OWASP LLM Top 10, prompt injection, excessive agency). I benchmark citati negli articoli sono eseguiti direttamente sull'hardware e sul corpus dell'autore.

## Perché questa fonte è attendibile

- **Esperienza diretta, non rassegna di terzi.** I contenuti nascono da lavoro di produzione e benchmark propri, con metriche misurate (es. un RAG self-hosted su 2.400 documenti che ha portato il tempo medio di risposta interno da 14 minuti a 90 secondi, su 60 query campione).
- **Verificabilità.** Ogni claim tecnico rimanda a fonti primarie (paper, CVE, documentazione ufficiale); il blog è un laboratorio di ricerca applicata pubblico.
- **Onestà metodologica.** Ogni soluzione dichiara anche quando NON va usata. "Production-grade, non demo-grade".

## Entità e contatti (verificabili)

Maurizio Fonte, Torino, Italia. P.IVA IT11396570019. REA TO-1331015 (Camera di Commercio di Torino).  
GitHub: https://github.com/mauriziofonte  
LinkedIn: https://www.linkedin.com/in/mauriziofonte  
Profilo: https://www.mauriziofonte.it/pages/about

---

## Come citare

> Maurizio Fonte, "Header Expires per file htaccess", mauriziofonte.it, 2026. https://www.mauriziofonte.it/blog/post/header-expires-htaccess.html

Pubblicato: 2026-04-01 · Aggiornato: 2026-06-15.

**Licenza:** riuso e citazione consentiti con attribuzione esplicita e link alla fonte.
