PHP - Setting the correct Cache Headers (client side caching)
New here? Learn about Bountify and follow @bountify to get notified of new bounties! x

I have a PHP app which creates images on the fly. Once the images are created for the first time, they are stored on a cache folder. Upon retrieving them from the cache folder I need to output the correct cache headers for proper client side caching.

I have no idea what's the right way of doing it. Requirements:

  • If image was recently modified, it should replace the cache of the user, and image should be requested from our server again.
  • If image was not modified since last request of same image, it should send the right headers so image is fetched from client-side cache without requesting from our server.

Please investigate on:

  • Should we use ETAG and EXPIRE headers?
  • Should we use NOT MODIFIED header?

This is all so confusing. I hope a developer experienced in this can solve this for me in five minutes avoiding having me to do all the research. Thanks!

This is my current (incomplete) function:

  * Passes through image from cache folder to render. 
  * @param string $fileName
  * @param string $fileExtension
 public static function renderImageFromCacheFolder($fileName, $fileExtension){
     $filePath = self::getImagesCacheFolderPath(). $fileName . ".". $fileExtension;

    //Modify last access time by using touch, so when we run cleanCache we can delete all images not accessed in the last 30 days

    header("Content-Type: image/$fileExtension");
    header('Content-Length: ' . filesize($filePath));

    //Client side caching
    header('Last-Modified: '.gmdate('r', filemtime($filePath))); 

    echo file_get_contents($filePath);


Bounty: Please fix my function and correct headers for client side caching using best practices.


awarded to alv-c

Crowdsource coding tasks.

1 Solution

Winning solution

I think changing your touch call to touch($filePath, date('U', filemtime($filePath)), time()) should do it.

I explained how it works at your other bounty.


I edited your method. Code is commented so I think it's pretty easy to understand how it works

Edit 2:

Here is what I think is the final version

Edit 3:

Here is your code modified

Edit 4:

final code

What about etag, public caché and all the other headers. Why are they necessary or not? Could you please post a complete solution? Thank you very much!
georgefountain 3 months ago
Anyone can clarify what do with etag and the other headers? Thanks
georgefountain 3 months ago
Edited solution!.
alv-c 3 months ago
Hi Alv! Thanks so much for editing the solution. It is all clear except one line. if (@strtotime($SERVER['HTTPIFMODIFIEDSINCE'])==$lastModified || $etagHeader == $etagFile) Are you sure about this line? What if the cache file was modified (filenames don't change)? The Etag woould be the same. Did you mean to put the AND (&&) operator?
georgefountain 3 months ago
Shouldn't the code be like this? if(isset($SERVER['HTTPIFMODIFIEDSINCE'])){ if(@strtotime($SERVER['HTTPIFMODIFIEDSINCE'])==$lastModified){ $notModified=true; } else{ $notModified=false; } }elseif($etagHeader == $etagFile){ $notModified=true; } if($notModified){ header("HTTP/1.1 304 Not Modified"); exit; } If so, please modify the file. Or explain. Thank you so much!!!
georgefountain 3 months ago
Edited again. Sorry for that error!. Now it uses the defined variable $ifModifiedSince, that was set before. And also left a comment explaining the use of || instead of &&
alv-c 3 months ago
Thanks Alv for the quick reply! I just edited your last answer, which what I was trying to say. Easier to read on the pastebin. Please kindly check it, and if ok, then I can award the bounty to you :) Thanks again!
georgefountain 3 months ago
The concept is, only check for etag, if $ifModifiedSince is not set. Because since the filepath never changes, even if the file is modifies the md5 will be the same. That is what I was trying to say.. although I am not sure if my code logic is correct.
georgefountain 3 months ago
Edited!. Changed your code to use $ifModifiedSince instead of $_SERVER['HTTP_IF_MODIFIED_SINCE'] (and btw, you wrote $SERVER['HTTPIFMODIFIEDSINCE']).
alv-c 3 months ago
Thanks! I got an idea!!! What if we make the Etag hash include the modification date? Check this out: . Now I believe the code is perfectly safe and an ETAG will only be the same if it is referring to the same filepath and modification time.
georgefountain 3 months ago
What do you think?
georgefountain 3 months ago
Hmm. I think you should keep both. Here's a good explaination why use both
alv-c 3 months ago
Oh but I didn't remove ETAG. I just changed the way the ETAG was generated and included last modification date. This was my only change: $etagFile = md5_file($filePath . lastModified); ... what do you think?
georgefountain 3 months ago
Oh. Then I think it's perfecty fine
alv-c 3 months ago
Great! So this is our final code: ?
georgefountain 3 months ago
Edited solution with final code. In your code there was an error at line 19. missing $ sign at lastModified variable and you concatenated the last modification date to the path of the image, but you needed to concatenate the last modification time to the result of calling md5_file of the image path. As I said, code is now in solution as final code
alv-c 3 months ago
Thank you Alv! :)
georgefountain 3 months ago
What do you think about changing public cache to private cache? Thanks
georgefountain 2 months ago
Client will cache the response either with public or private cache. But private cache doesn't allow proxies to cache the response. See more
alv-c 2 months ago
Thanks! Another question, why are you ignoring warnings on strtotime(? What are the expected inputs that could cause a warning and why you added the @?
georgefountain 2 months ago
Let me know about this when you can. Thanks :)
georgefountain 2 months ago
In this case, the input comes from reliable source so there's no possible error. But errors can come from bad time parameter, or php.ini missconfiguration
alv-c 2 months ago
View Timeline