Existem várias coisas que você pode fazer aqui. Por um lado, você menciona que você serve as imagens com readfile ()? Isso pode ser um culpado aqui, já que é o PHP que está servindo as imagens. Isso não é inerentemente muito mais lento do que servi-las estáticas e vem com vários benefícios (como o processamento dinâmico de imagens), mas você pode ter várias armadilhas como resultado. Do ponto de vista do servidor, é sempre muito mais rápido exibir imagens como estáticas.
Primeiro de tudo, é o seu script - aquele que retorna imagens - empurrando cabeçalhos corretos? Ou seja, você está enviando Cache-control e expira cabeçalhos que dizem aos agentes do usuário para armazenar imagens em cache? Seu tráfego pode vir do fato de suas imagens serem veiculadas o tempo todo ou com muita frequência. Envie também um cabeçalho Last-modified.Você também pode enviar um cabeçalho Não modificado como uma resposta se um agente do usuário fizer uma solicitação para um arquivo e você souber que ele não foi modificado desde a data que o agente do usuário fornece.
Se você já configurou todos os cabeçalhos adequados e certificou-se de que os user-agents não estão solicitando inutilmente os dados que eles já possuem, então seu problema está no seu servidor web. Você fez algum teste de desempenho para ver se isso afeta apenas as imagens ou se afeta todas as solicitações em geral?
Eu sugiro tentar ver o quão poderoso seu servidor é ao servir solicitações regulares (PHP). Veja quanto tempo suas páginas demoram e compare-as com as solicitações de uma imagem. Eu sugiro uma boa ferramenta como Pylot para isso (é linha de comando, mas realmente útil).
(Note que, para comparação, é útil comparar uma página com tamanho X a uma imagem de tamanho X, de modo que a largura de banda seja igual e você possa apenas avaliar o desempenho).
EDITAR:
Os cabeçalhos de cache que informam aos agentes do usuário que fazem a solicitação para armazenar em cache o arquivo pelo maior tempo possível são os seguintes:
$lastModified=filemtime($myPicture); // File location of your image
$cacheDuration=31536000; // One year
// This tells user agent to keep the cache for one year
header('Cache-Control: public,max-age='.$cacheDuration);
header('Expires: '.gmdate('D, d M Y H:i:s',(time()+$cacheDuration)).' GMT');
// This tells the user agent the last modified time of your image
header('Last-Modified: '.gmdate('D, d M Y H:i:s',$lastModified).' GMT');
Exibição de cabeçalhos não modificados é um pouco mais complicada:
// Testing if the servers known last modified time is different from the last modified time of file on your server
// If those times are the same, then we will not return the image and just tell the user agent to use their cache
if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])>=$lastModified){
// Cache headers (Last modified is never sent with 304 header, since it is often ignored)
header('Cache-Control: public,max-age='.$cacheDuration);
header('Expires: '.gmdate('D, d M Y H:i:s',(time()+$cacheDuration)).' GMT');
// Returning 304 header
header('HTTP/1.1 304 Not Modified');
die();
}