O uso de cache é uma das formas mais eficazes de acelerar aplicações web, porque evita trabalho repetido em cada requisição. Em projetos Django, o cache reduz consultas ao banco, diminui a latência e melhora a capacidade de atender muitos acessos simultâneos com a mesma infraestrutura.
Nesse contexto, o Redis se destaca por ser um armazenamento em memória extremamente rápido, adequado para guardar dados temporários com expiração. A combinação de Django com Redis permite aplicar cache em vários níveis, como valores simples, resultados de consultas, páginas inteiras, fragmentos de template e até sessões, mantendo um equilíbrio entre desempenho e atualização dos dados.
Conceitos essenciais: cache, Redis e expiração (TTL)
Cache é uma área de armazenamento temporário usada para reutilizar resultados já calculados ou já buscados. O objetivo é reduzir operações custosas, como consultas ao banco de dados ou cálculos pesados. Redis é um armazenamento em memória que mantém dados em estruturas simples e responde muito rápido. Um conceito central é o TTL (tempo de vida), que define em quantos segundos um item expira e deixa de ser usado para evitar dados desatualizados.
Em Django, o cache é acessado por uma API única, independente do mecanismo por trás. Isso permite trocar o backend de cache sem alterar o restante do código, desde que a configuração seja ajustada. O Redis é comum por oferecer bom desempenho, expiração nativa e recursos úteis para produção. Uma estratégia correta decide o que armazenar, por quanto tempo e como invalidar quando dados mudam.
Instalação do Redis no sistema operacional
A instalação do Redis depende do sistema, mas o objetivo é o mesmo: iniciar o serviço e garantir que ele suba automaticamente. Em Linux (Debian/Ubuntu), o Redis geralmente é instalado via gerenciador de pacotes e controlado por systemd. Em macOS, a instalação costuma ser feita via Homebrew e o serviço pode ser iniciado como daemon. Em Windows, o caminho mais comum é usar WSL (Windows Subsystem for Linux) para rodar Redis de forma compatível.
Os comandos abaixo exemplificam instalação e inicialização em ambientes comuns. Eles mostram atualização de pacotes, instalação do serviço e ativação automática na inicialização. Após instalar, é importante validar a conectividade com o utilitário de linha de comando. A verificação mais simples é um “ping” que retorna “PONG”.
# Ubuntu/Debian
sudo apt update
sudo apt install redis-server
sudo systemctl start redis
sudo systemctl enable redis
# Verificar funcionamento
redis-cli ping # deve responder PONG
# macOS (Homebrew)
brew install redis
brew services start redis
# Verificar funcionamento
redis-cli ping # deve responder PONG
Dependências Python: redis e django-redis
Para integrar Django com Redis, é comum usar a biblioteca django-redis, que implementa o backend de cache do Django usando Redis. A biblioteca redis é o cliente Python que faz a comunicação com o servidor Redis. Juntas, elas fornecem configuração, pooling de conexões e operações de cache pela API padrão do Django. Fixar versões no arquivo de dependências ajuda a manter builds reprodutíveis.
O pacote django-redis também expõe recursos úteis para diagnósticos e acesso ao cliente nativo quando necessário. Em produção, versões podem ser gerenciadas por requirements.txt ou por ferramentas de lockfile. O importante é garantir compatibilidade entre Django, django-redis e redis-py. A instalação a seguir cobre o cenário típico.
pip install redis django-redis
redis==5.0.1
django-redis==5.4.0
Configuração do cache no Django (settings.py) com Redis
A configuração do cache fica em CACHES no settings.py e define qual backend será usado, onde ele está e como se comporta. O parâmetro LOCATION aponta para o Redis, geralmente no formato de URL, incluindo host, porta e um índice de banco lógico. O KEY_PREFIX adiciona um prefixo a todas as chaves para evitar colisões entre aplicações ou ambientes. O TIMEOUT define a expiração padrão em segundos para itens que não informarem um timeout específico.
Algumas opções refinam a robustez em produção, como limites de conexões e timeouts de socket. O pool de conexões controla quantas conexões simultâneas podem ser abertas com Redis, o que impacta concorrência. Timeouts de conexão e de leitura protegem a aplicação caso o Redis fique lento ou indisponível. Quando Redis possui senha, ela pode ser informada em OPTIONS, mas em muitos ambientes a proteção ocorre por rede isolada e políticas do servidor.
# settings.py
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"PASSWORD": "sua-senha-redis", # opcional, se configurada no Redis
"SOCKET_CONNECT_TIMEOUT": 5,
"SOCKET_TIMEOUT": 5,
"CONNECTION_POOL_KWARGS": {
"max_connections": 50,
"retry_on_timeout": True,
},
},
"KEY_PREFIX": "minhaapp",
"TIMEOUT": 300, # 5 minutos
}
}
Operações básicas de cache: set, get, delete e existência
A API de cache do Django oferece operações diretas para armazenar e recuperar valores. O método set grava um valor associado a uma chave e aceita um timeout por item. O método get retorna o valor armazenado ou None caso não exista ou tenha expirado. O método delete remove uma chave explicitamente, útil quando dados mudam e o cache precisa ser invalidado. Também existe verificação de existência, embora seja preciso cautela porque checar e usar depois não é atômico em cenários concorrentes.
Chaves devem ser estáveis, previsíveis e específicas, evitando nomes genéricos. Timeouts curtos reduzem risco de conteúdo desatualizado, mas aumentam o número de recomputações. Timeouts longos melhoram desempenho, mas exigem invalidação cuidadosa. O exemplo abaixo demonstra o ciclo completo de escrita, leitura e remoção.
from django.core.cache import cache
# Armazenar um valor simples
cache.set("exemplo:minha_chave", "meu_valor", timeout=300)
# Ler o valor
valor = cache.get("exemplo:minha_chave")
print(valor) # imprime "meu_valor"
# Remover o valor
cache.delete("exemplo:minha_chave")
# Verificar existência (uso com cautela em cenários concorrentes)
existe = cache.has_key("exemplo:minha_chave") # noqa: W601 (método existe em alguns backends)
print(existe)
Cache de dados complexos e múltiplos valores (set_many e get_many)
Além de strings e números, o cache pode armazenar estruturas como dicionários e listas, desde que sejam serializáveis pelo backend. No caso do django-redis, a serialização padrão costuma usar pickle, o que facilita armazenar objetos Python, mas exige atenção com compatibilidade e segurança em ambientes controlados. Dados complexos devem ser pequenos o suficiente para não consumir memória excessiva. Quando o mesmo conjunto de chaves é usado com frequência, operações em lote reduzem overhead.
As operações set_many e get_many permitem escrever e ler várias chaves de uma vez. Isso diminui idas e voltas de rede e pode melhorar a latência em endpoints que precisam de diversos itens. Chaves com prefixos de domínio, como “usuario:123”, ajudam a organizar e a invalidar por padrão. Os exemplos abaixo mostram armazenamento de dicionário e uso de operações em lote.
from django.core.cache import cache
dados_usuario = {
"id": 123,
"nome": "João da Silva",
"email": "joao@exemplo.com",
}
cache.set("usuario:123", dados_usuario, timeout=600)
usuario_em_cache = cache.get("usuario:123")
print(usuario_em_cache)
from django.core.cache import cache
cache.set_many(
{
"chave:1": "valor1",
"chave:2": "valor2",
"chave:3": "valor3",
},
timeout=300,
)
valores = cache.get_many(["chave:1", "chave:2", "chave:3"])
print(valores) # {'chave:1': 'valor1', 'chave:2': 'valor2', 'chave:3': 'valor3'}
Cache de consultas ao banco: padrão cache-aside
O ganho mais comum vem de reduzir consultas repetidas ao banco de dados, principalmente em listas, páginas iniciais e áreas com alto tráfego. O padrão cache-aside significa tentar ler do cache primeiro e, em caso de ausência, consultar o banco e então preencher o cache. Isso evita consultas idênticas em sequência e reduz o trabalho do ORM. Um detalhe prático é transformar QuerySets em lista antes de armazenar, porque QuerySet é preguiçoso e pode executar consulta fora do momento esperado.
Um ponto crítico é escolher uma chave que represente exatamente o conjunto de dados, incluindo filtros e ordenações relevantes. Também é importante definir um timeout compatível com a frequência de atualização da informação. Quando dados mudam com frequência, timeouts menores ou invalidação ativa são mais seguros. O código abaixo contrasta uma view que sempre consulta o banco com uma versão que usa cache-aside.
from django.shortcuts import render
from myapp.models import Product
def lista_produtos_sem_cache(request):
produtos = Product.objects.filter(ativo=True).select_related("categoria")
return render(request, "produtos.html", {"produtos": produtos})
from django.core.cache import cache
from django.shortcuts import render
from myapp.models import Product
def lista_produtos_com_cache(request):
chave_cache = "produtos:ativos:v1"
produtos = cache.get(chave_cache)
if produtos is None:
produtos = list(
Product.objects.filter(ativo=True).select_related("categoria")
)
cache.set(chave_cache, produtos, timeout=600) # 10 minutos
return render(request, "produtos.html", {"produtos": produtos})
Cache de views: cache_page e variações por usuário
Quando uma view gera sempre o mesmo resultado para muitas requisições, o cache de página inteira reduz drasticamente o trabalho do servidor. O decorator cache_page armazena a resposta HTTP completa por um período, evitando executar a view repetidas vezes. Isso é especialmente útil para páginas públicas, catálogos e conteúdos que mudam pouco. A validade precisa ser compatível com a atualização do conteúdo, para evitar servir informações antigas.
Para conteúdo que depende do usuário logado, pode ser necessário variar o cache por cookie ou cabeçalhos. O decorator vary_on_cookie instrui o cache a separar versões por cookie, o que costuma diferenciar usuários autenticados. Essa técnica reduz risco de um usuário receber a página de outro, mas aumenta consumo de cache porque multiplica as versões armazenadas. Em muitos casos, fragmentos específicos por usuário são melhores do que cachear a página inteira por usuário.
from django.views.decorators.cache import cache_page
from django.shortcuts import render
from myapp.models import Product
@cache_page(60 * 15) # 15 minutos
def lista_produtos_cache_pagina(request):
produtos = Product.objects.all()
return render(request, "produtos.html", {"produtos": produtos})
from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_cookie
from django.shortcuts import render
@cache_page(60 * 15)
@vary_on_cookie
def painel_usuario(request):
return render(request, "painel.html")
Invalidação automática: sinais (signals) e remoção de chaves
Cache eficiente não depende apenas de expiração por tempo, mas também de invalidação, que é remover ou atualizar entradas quando os dados mudam. Em Django, uma abordagem comum é usar signals (sinais), como post_save, para reagir a salvamentos no banco. Quando um produto é alterado, por exemplo, chaves relacionadas podem ser removidas para forçar recomputação na próxima requisição. Esse mecanismo reduz a janela de inconsistência entre o banco e o cache.
O desenho das chaves influencia diretamente a invalidação, pois listas agregadas e detalhes individuais podem exigir remoções diferentes. Uma alteração em um item pode afetar a lista de “ativos”, além do cache do próprio item. Em sistemas maiores, invalidação pode exigir padrões mais sofisticados, como versionamento de chaves. O exemplo a seguir mostra remoção do cache do item e também da lista agregada.
from django.core.cache import cache
from django.db.models.signals import post_save
from django.dispatch import receiver
from myapp.models import Product
@receiver(post_save, sender=Product)
def invalidar_cache_produto(sender, instance, **kwargs):
cache.delete(f"produto:{instance.id}")
cache.delete("produtos:ativos:v1")
Cache de cálculos custosos: agregações e relatórios
Nem todo gargalo é consulta repetida simples, pois relatórios e agregações podem levar segundos. Um caso típico é calcular receita mensal ou métricas que fazem varreduras em tabelas grandes. Armazenar esse resultado por um período razoável evita repetir um cálculo pesado para cada acesso. O timeout costuma ser maior, porque relatórios toleram certo atraso na atualização.
Mesmo em cálculos cacheados, é importante definir comportamento para valores vazios e para o caso de cache indisponível. A chave deve indicar claramente o período e o tipo de métrica. Quando há múltiplos filtros, as chaves precisam refletir esses filtros para não misturar resultados. O exemplo abaixo calcula um total agregado e guarda por uma hora.
from datetime import timedelta
from django.core.cache import cache
from django.db.models import Sum
from django.utils import timezone
from myapp.models import Order
def obter_receita_ultimos_30_dias():
chave_cache = "relatorio:receita_30_dias:v1"
receita = cache.get(chave_cache)
if receita is None:
inicio = timezone.now() - timedelta(days=30)
receita = (
Order.objects.filter(criado_em__gte=inicio)
.aggregate(total=Sum("valor"))["total"]
)
cache.set(chave_cache, receita, timeout=3600) # 1 hora
return receita
Cache de fragmentos de template: foco no trecho caro
O cache de fragmentos permite armazenar apenas partes do HTML que custam caro para renderizar, sem cachear a página inteira. Em Django, isso é feito no template com a tag cache, que recebe o tempo e uma chave base. É útil em barras laterais, menus de categorias e blocos de recomendação que mudam pouco. Esse método preserva áreas dinâmicas da página, como mensagens personalizadas, sem multiplicar versões completas de páginas.
Fragmentos podem variar por parâmetros, como o id do usuário, quando o bloco é específico. Essa variação precisa ser usada com cautela, pois aumenta o número de entradas. Em muitos cenários, variar por idioma, tema ou grupo é suficiente, evitando granularidade excessiva. Os exemplos abaixo mostram cache de um bloco genérico e um bloco que varia por usuário.
{% load cache %}
{% cache 500 sidebar %}
<div class="sidebar">
{% for categoria in categorias %}
<a href="{{ categoria.url }}">{{ categoria.name }}</a>
{% endfor %}
</div>
{% endcache %}
{% load cache %}
{% cache 500 sidebar request.user.id %}
<div class="sidebar">
Conteúdo específico do usuário.
</div>
{% endcache %}
Sessões no Redis: menos carga no banco e expiração natural
Outra aplicação comum do Redis em Django é armazenar sessões, que são dados associados à navegação autenticada de um usuário. Em vez de salvar sessões no banco relacional, o Redis oferece leitura e escrita mais rápidas e expiração automática. Isso reduz consultas ao banco, especialmente em aplicações com muitos usuários logados. A configuração usa o backend de sessões baseado em cache do próprio Django.
O cache de sessões deve ser confiável, pois impacta autenticação e estado de navegação. Em ambientes com múltiplas instâncias de aplicação, o Redis centraliza as sessões e evita inconsistência. A expiração segue as regras de sessão do Django e o Redis remove itens vencidos sem esforço extra. A configuração abaixo aponta as sessões para o cache default.
# settings.py
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default"
Monitoramento e diagnóstico: estatísticas e limpeza do cache
Monitorar o cache ajuda a confirmar se o ganho esperado está acontecendo e se o Redis não está saturando memória. Um indicador importante é a taxa de acerto, conhecida como hit ratio, que representa quantas leituras foram atendidas pelo cache. Também é útil observar memória usada e número de clientes conectados. Em django-redis, é possível acessar o cliente nativo e consultar informações do servidor.
A limpeza completa com cache.clear remove todas as chaves daquele namespace e pode causar aumento súbito de carga no banco, pois tudo vira “cache miss”. Por isso, essa operação costuma ser reservada para manutenção controlada. Em diagnósticos pontuais, remover chaves específicas é mais seguro. O exemplo abaixo mostra como obter estatísticas básicas e como limpar com cautela.
from django.core.cache import cache
cliente_redis = cache.client.get_client()
info = cliente_redis.info()
print(f"Memória usada: {info.get('used_memory_human')}")
print(f"Clientes conectados: {info.get('connected_clients')}")
from django.core.cache import cache
cache.clear() # remove todas as chaves do cache configurado
Boas práticas: chaves significativas, timeouts e tolerância a falhas
Boas práticas evitam desperdício de memória e reduzem riscos de dados errados. Chaves significativas facilitam manutenção, invalidação e depuração, além de evitar colisões. Timeouts devem refletir a natureza do dado, pois dados estáticos podem ficar mais tempo em cache e dados dinâmicos exigem expiração curta. Também é importante tratar falhas do cache, porque Redis pode ficar indisponível e a aplicação deve continuar funcionando com fallback ao banco.
Outro cuidado é evitar armazenar dados sensíveis específicos de usuário, como credenciais e informações financeiras, sem um desenho seguro e adequado. Mesmo quando há criptografia, o cache é um local de alta exposição operacional. Em produção, é comum registrar erros de cache e seguir com o fluxo normal da aplicação. O exemplo abaixo ilustra chaves boas e um padrão de fallback simples com tratamento de exceção.
- Chaves significativas: usar prefixos como "usuario:perfil:123" em vez de nomes genéricos.
- Timeout adequado: estático (ex.: 24h), semiestático (ex.: 1h), dinâmico (ex.: 5–15 min).
- Falha controlada: em erro de cache, buscar do banco e não interromper a resposta.
import logging
from django.core.cache import cache
from myapp.servicos import buscar_dados_no_banco
logger = logging.getLogger(__name__)
def obter_dados_com_fallback():
chave_cache = "relatorio:resumo:v1"
try:
dados = cache.get(chave_cache)
except Exception as exc:
logger.error(f"Erro ao acessar cache: {exc}")
dados = None
if dados is None:
dados = buscar_dados_no_banco()
cache.set(chave_cache, dados, timeout=900) # 15 minutos
return dados
Erros comuns: cachear tudo, não expirar e ignorar invalidação
Cache mal aplicado pode piorar o sistema, consumindo memória e aumentando complexidade sem ganho real. Cachear dados que mudam o tempo todo tende a desperdiçar espaço, porque o item expira antes de ser reutilizado ou é sobrescrito constantemente. Manter itens sem expiração aumenta risco de servir dados obsoletos e torna difícil entender comportamentos inesperados. Ignorar invalidação faz com que alterações no banco não apareçam no sistema até o timeout acabar, o que pode ser inaceitável.
Uma regra prática é cachear apenas o que é repetido, caro e relativamente estável. Quando dados são críticos e mudam com frequência, a invalidação por evento ou por versionamento é mais adequada do que timeouts longos. Também é importante não armazenar grandes objetos sem necessidade, porque Redis trabalha em memória. O exemplo abaixo mostra uma atualização de produto que remove a chave correspondente após salvar.
from django.core.cache import cache
from myapp.models import Product
def atualizar_produto(produto_id, novos_dados):
produto = Product.objects.get(id=produto_id)
produto.nome = novos_dados["nome"]
produto.save()
cache.delete(f"produto:{produto_id}")
Encerramento: equilíbrio entre desempenho, frescor e custo
O cache com Redis em Django melhora desempenho ao evitar recomputações e reduzir leituras no banco, principalmente em listas, páginas públicas e relatórios pesados. A eficácia depende de três pilares: escolha correta do que cachear, timeouts coerentes e invalidação confiável quando os dados mudam. O Redis também se encaixa bem como armazenamento de sessões, reduzindo carga no banco e aproveitando expiração natural.
Um resultado consistente vem de aplicar cache em camadas, começando por gargalos claros e evoluindo para fragmentos e invalidação automática. A abordagem madura evita cachear dados inúteis e prioriza chaves bem definidas, monitoramento e tolerância a falhas. Com esse conjunto de práticas, o cache deixa de ser apenas uma configuração e passa a ser um componente estável de desempenho e previsibilidade da aplicação.