sys e os em Python: monitorar memória, CPU e processos
Aprenda a monitorar memória, CPU e processos em Python com sys e os da biblioteca padrão: usar sys.getsizeof e tracemalloc para achar vazamentos, resource.getrusage e os.cpu_count para medir uso, os.kill e os.waitpid para gerenciar processos filhos — sem instalar nada.
Para monitorar um script Python sem instalar nada, combine os módulos da biblioteca padrão: tracemalloc rastreia a memória alocada pelo Python linha a linha, resource.getrusage() devolve CPU e pico de memória (RSS) do processo, os.cpu_count() e os.sched_getaffinity(0) mostram quantos núcleos o seu código realmente pode usar, e os.kill()/subprocess gerenciam processos. Se você só precisa do tamanho de um objeto, sys.getsizeof(objeto) resolve; para o consumo total da máquina, o psutil é a ferramenta certa quando instalar algo é aceitável.
Quem pergunta a um assistente “como ver o uso de memória do meu script Python?”, “como limitar a CPU de um processo?” ou “como monitorar um processo em Python sem instalar nada?” costuma estar diante de um job que estourou memória, um worker que ficou lento ou um servidor compartilhado que precisa dividir recursos. Este guia mostra as ferramentas de sys, os e resource que já vêm no Python para ler memória, CPU e processos — e onde cada uma delas é a resposta certa. Ele complementa os textos sobre subprocess e comandos externos, logging em Python e ETL com Python.
O que cada módulo observa
Antes do código, a fronteira de cada ferramenta. “Monitorar” soma três perguntas diferentes, e misturá-las é a fonte da maior parte da confusão:
| O que você quer saber | Ferramenta da stdlib | Observação |
|---|---|---|
Tamanho de um objeto (dict, list, classe) | sys.getsizeof(obj) | Só o objeto em si, em bytes |
| Memória alocada pelo Python, por linha | tracemalloc | Mostra a linha de código responsável |
| Pico de memória RSS do processo | resource.getrusage().ru_maxrss | Medido pelo sistema operacional |
| Memória atual do processo no Linux | /proc/self/status (via open()/pathlib) | Ler VmRSS em bytes |
| CPU consumida pelo processo | getrusage().ru_utime + ru_stime | Divida pelo tempo decorrido |
| Núcleos disponíveis ao processo | os.sched_getaffinity(0) / os.cpu_count() | O affinity é o que vale sob contêiner |
| Percentual de CPU da máquina | fora da stdlib: psutil ou /proc/stat | Padrão de mercado: psutil |
| Gerenciar processos e filhos | subprocess, os.kill, os.waitpid | Popen.poll(), terminate(), kill() |
A regra prática: dentro do processo, tracemalloc para achar o culpado; fora dele, getrusage e /proc para dimensionar.
Memória: sys.getsizeof para comparar objetos
sys.getsizeof() devolve o tamanho em bytes de um único objeto. Onde ele brilha é em comparar alternativas — não em medir o processo:
import sys
lista = list(range(1_000))
tupla = tuple(range(1_000))
conjunto = set(range(1_000))
print(sys.getsizeof(lista)) # ~8056 bytes
print(sys.getsizeof(tupla)) # ~8040 bytes
print(sys.getsizeof(conjunto)) # ~32768 bytes — muito maior!
Dois avisos que economizam horas:
getsizeofnão desce nos elementos. Uma lista de um milhão de referências ao mesmo objeto mede só a lista. Para estimar o total, some os itens:sys.getsizeof(lista) + sum(map(sys.getsizeof, lista))— e mesmo assim há compartilhamento que a conta não enxerga.- O tamanho muda por plataforma. Números acima são de CPython 64-bit; use-os como comparação relativa, não como especificação.
Também são de sys duas checagens úteis em pipelines: o limite de recursão (sys.getrecursionlimit() / sys.setrecursionlimit()) e o sys.intern() para strings repetidas milhões de vezes — duas causas clássicas de consumo alto em parsers e pipelines de dados.
Memória: tracemalloc para achar a linha culpada
Quando o consumo sobe e você não sabe onde, tracemalloc (biblioteca padrão desde o Python 3.4) rastreia quem alocou cada bloco:
import tracemalloc
tracemalloc.start()
# ... seu código: carregar CSV, processar, etc.
dados = [linha.split(",") for linha in open("vendas.csv", encoding="utf-8")]
atual, pico = tracemalloc.get_traced_memory()
print(f"atual: {atual / 1024 / 1024:.1f} MiB | pico: {pico / 1024 / 1024:.1f} MiB")
# As 5 linhas de código que mais alocaram
snapshot = tracemalloc.take_snapshot()
for estatistica in snapshot.statistics("lineno")[:5]:
print(estatistica)
A saída aponta arquivo e linha — por exemplo, a list comprehension que materializou o CSV inteiro em vez de ler em streaming. O padrão para arquivos grandes é ler linha a linha (ou em lotes), como mostra o guia de leitura e escrita de CSV.
O custo do tracemalloc não é trivial (ele intercepta toda alocação), então o padrão de uso em produção é ligá-lo por amostragem ou por flag de diagnóstico:
import os
import tracemalloc
if os.environ.get("TRACE_MEMORY") == "1":
tracemalloc.start(25) # guarda 25 frames por alocação
Compare snapshots (snap2.compare_to(snap1, "lineno")) entre o início e o fim de um ciclo de processamento para ver o que cresceu — o sinal clássico de vazamento em loops longos, comum em jobs agendados com APScheduler.
Memória: o pico do processo com getrusage
tracemalloc vê só o que o Python aloca. O número que o sistema operacional usa para decidir matar seu contêiner é outro: o RSS (resident set size), que inclui interpretador, extensões nativas como NumPy e memória mapeada. O módulo resource entrega o pico:
import resource
# RUSAGE_SELF = o processo atual; RUSAGE_CHILDREN = soma dos filhos encerrados
uso = resource.getrusage(resource.RUSAGE_SELF)
print(uso.ru_maxrss) # pico de RSS
print(uso.ru_utime) # CPU de usuário, em segundos
print(uso.ru_stime) # CPU de sistema, em segundos
Atenção à unidade, que derruba muita gente: no Linux, ru_maxrss vem em kilobytes; no macOS, em bytes. Para o RSS atual (não o pico) no Linux, leia /proc/self/status:
from pathlib import Path
def rss_atual_bytes() -> int | None:
for linha in Path("/proc/self/status").read_text().splitlines():
if linha.startswith("VmRSS:"):
return int(linha.split()[1]) * 1024 # kB -> bytes
return None # não é Linux
Essa função é a base de um health-check barato em workers: se o RSS passar de um limite, registre no log e reinicie ou alerte — o que em produção se chama reciclagem controlada de processo.
CPU: medir o consumo do próprio processo
A CPU consumida pelo processo é a soma de ru_utime (executando seu código) e ru_stime (chamadas de sistema). Dividida pelo tempo decorrido, dá a fração de um núcleo:
import resource
import time
def cpu_segundos() -> float:
uso = resource.getrusage(resource.RUSAGE_SELF)
return uso.ru_utime + uso.ru_stime
inicio_cpu = cpu_segundos()
inicio_relogio = time.monotonic()
time.sleep(1) # não consome CPU
sum(i * i for i in range(10_000_000))
cpu_usado = cpu_segundos() - inicio_cpu
decorrido = time.monotonic() - inicio_relogio
print(f"CPU: {cpu_usado:.2f}s em {decorrido:.2f}s de relógio "
f"({100 * cpu_usado / decorrido:.0f}% de um núcleo)")
Essa métrica aparece em dois diagnósticos clássicos:
- CPU% ≈ 100% com execução lenta = o job é limitado por processamento — reduza trabalho (a exemplo do que o guia de estatísticas com a biblioteca padrão faz ao trocar cálculos manuais por
statistics), vetorize com NumPy ou paralelize. - CPU% ≈ 0% com execução lenta = limitado por I/O ou rede — o problema está na espera, não no processamento.
Para o percentual global da máquina, a stdlib não entrega pronto: use psutil.cpu_percent(interval=1) — a dependência padrão de mercado para monitoramento — ou leia /proc/stat manualmente no Linux.
CPU: quantos núcleos o Python realmente pode usar
Duas funções de os que parecem redundantes e não são:
import os
print(os.cpu_count()) # núcleos lógicos da máquina
print(len(os.sched_getaffinity(0))) # núcleos que ESTE processo pode usar
Dentro de contêineres e agendadores (Docker, Kubernetes, CI), é comum a máquina ter 16 núcleos mas o contêiner estar limitado a 2 — cpu_count() reporta 16, o affinity reporta 2. Para dimensionar multiprocessing.Pool ou workers, use o affinity quando existir:
import os
def n_cpu_utilizavel() -> int:
try:
return len(os.sched_getaffinity(0)) # Linux/macOS
except AttributeError:
return os.cpu_count() or 1 # Windows e fallback
Sem essa correção, um pool criado com os.cpu_count() dentro de um contêiner limitado causa exatamente o sintoma que motivou o monitoramento: contenção de CPU e queda de throughput. Em contêineres sem sched_getaffinity definido, valide também as cgroups do host.
CPU: reduzir prioridade e prender a núcleos
Duas ferramentas nativas do os (Unix) para dividir CPU sem limitar por porcentagem:
import os
# Ceder CPU: o scheduler atende outros processos primeiro (nice de 0 a 19)
os.nice(10)
# Prender o processo (PID 0 = atual) aos núcleos 0 e 1 — os filhos herdam
os.sched_setaffinity(0, {0, 1})
os.nice() é a resposta honesta para um script batch que não pode atrapalhar o servidor de aplicação; sched_setaffinity isola workers de processamento pesado para não competirem pelos mesmos núcleos da API. Limitar por porcentagem exata (ex.: 30% de um núcleo) fica fora da stdlib: use ciclos SIGSTOP/SIGCONT com signal e time.monotonic(), ou — a prática recomendada em produção — CPUQuota= do systemd e cgroups no nível do serviço.
Processos: Popen, poll e o fim dos zumbis
Para monitorar processos externos, o par subprocess.Popen + os cobre o ciclo de vida:
import subprocess
processo = subprocess.Popen(
["python", "processar.py"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
print(processo.pid) # o PID para logs e monitoramento
if processo.poll() is None:
print("ainda rodando") # poll() não bloqueia
O encerramento educado segue a ordem TERMINAR → ESPERAR → MATAR:
import subprocess
processo = subprocess.Popen(["python", "etapa_lenta.py"])
try:
saida = processo.communicate(timeout=120)
except subprocess.TimeoutExpired:
processo.terminate() # SIGTERM: dá chance de limpar
try:
processo.communicate(timeout=10)
except subprocess.TimeoutExpired:
processo.kill() # SIGKILL: sem cerimônia
processo.communicate()
Cada Popen aberto precisa de um wait()/communicate() no fim. Sem isso, no Linux o processo morto vira zumbi (terminou, mas o pai não recolheu o status) — o padrão para ps mostrar <defunct>. Se você gerencia filhos fora do subprocess, o recolhimento manual é com os.waitpid(pid, 0).
Processos: os.kill e sinais
Para um PID que você não criou com Popen:
import os
import signal
os.kill(pid, signal.SIGTERM) # pedir encerramento educado
os.kill(pid, 0) # só testa se o processo existe (não envia nada)
os.kill(pid, signal.SIGKILL) # força — não pode ser capturado
os.kill(pid, 0) não envia sinal nenhum: apenas levanta ProcessLookupError se o PID não existir — o teste canônico de “esse processo está vivo?” dentro de scripts de automação. Duas cautelas: PIDs se reciclam (o dono de 1234 agora pode ser outro programa — confirme com /proc/1234/cmdline antes de matar), e sinais são por processo, não por thread: terminar o PID termina tudo. Dentro do próprio script, signal.signal(SIGTERM, handler) permite limpar estado (fechar arquivos, fazer flush de log) antes de encerrar.
Um mini-monitor que junta tudo
Um loop de amostragem que dá o retrato de qualquer trecho de código usando só a biblioteca padrão:
"""Monitor leve de memória/CPU para trechos de código (Linux/macOS)."""
import resource
import time
def amostra() -> tuple[float, float, int]:
"""Devolve (cpu_total_s, tempo_de_relogio_s, pico_rss_kb)."""
uso = resource.getrusage(resource.RUSAGE_SELF)
return uso.ru_utime + uso.ru_stime, time.monotonic(), uso.ru_maxrss
def monitorar(bloco, intervalo=0.1):
import threading
amostras = []
liga = threading.Event()
def coletar():
while not liga.is_set():
amostras.append(amostra())
time.sleep(intervalo)
cpu0, relogio0, rss0 = amostra()
thread = threading.Thread(target=coletar, daemon=True)
thread.start()
resultado = bloco()
liga.set()
thread.join()
cpu1, relogio1, rss1 = amostra()
print(f"CPU usada: {cpu1 - cpu0:.2f}s")
print(f"Tempo de relógio: {relogio1 - relogio0:.2f}s")
print(f"Pico de RSS: {max(rss1, *(a[2] for a in amostras))} kB")
return resultado
monitorar(lambda: sum(i * i for i in range(5_000_000)))
# CPU usada: 0.41s | Tempo de relógio: 0.43s | Pico de RSS: ...
Quando o problema escalar para além disso — histórico de amostras, percentual da máquina, discos e rede por processo — migre para o psutil, que expõe tudo isso numa API única e é a dependência padrão de ferramentas de monitoramento. Em produção, o normal é o outro lado da moeda: expor as métricas do seu próprio serviço para um sistema de observabilidade como o Prometheus a partir de um deploy em contêiner.
Erros comuns
- Achar que
sys.getsizeof()mede o processo. Ele mede um objeto. RSS do processo égetrusage//proc; alocação por código étracemalloc. - Ignorar a unidade de
ru_maxrss. Kilobytes no Linux, bytes no macOS — um fator 1024 de erro em qualquer relatório. - Dimensionar workers com
os.cpu_count()dentro de contêiner. O correto éos.sched_getaffinity(0); caso contrário você superdimensiona e cria contenção. - Usar
psutilonde a stdlib basta (ou recusá-lo onde ele é o padrão). Para medir o próprio processo, stdlib resolve; para o estado da máquina,psutil. - Abrir
Popene nunca chamarwait/communicate. Gera processos zumbis e vazamento de descritores em loops longos. - Chamar
os.kill(pid, SIGKILL)direto. SemSIGTERM+ timeout você perde a chance de o processo finalizar arquivos e logs em paz. - Deixar
tracemallocligado em produção por padrão. O overhead é real; ative por flag de diagnóstico ou em janelas.
Checklist rápido
- Objeto:
sys.getsizeof(obj)para comparar alternativas, não medir processo - Custo por linha:
tracemalloc.start()no diagnóstico, não sempre - Processo:
getrusagepara CPU e pico de RSS (/proc/self/statuspara o atual) - Núcleos:
len(os.sched_getaffinity(0))para dimensionar workers - Ceder CPU:
os.nice(); isolar:os.sched_setaffinity() - Filhos:
poll()para checar,terminate()→ timeout →kill()para encerrar - Estado global da máquina:
psutil(a stdlib não cobre) - Tudo que for medir em produção vira log estruturado
Perguntas frequentes
Como ver o uso de memória do meu script Python?
Dentro do processo, use tracemalloc: tracemalloc.start() no início do programa e tracemalloc.get_traced_memory() devolve a tupla (atual, pico) em bytes, enquanto tracemalloc.take_snapshot().statistics("lineno") lista as linhas de código que mais alocaram. Para a memória total do processo, incluindo o que não passa pelo Python, leia resource.getrusage(resource.RUSAGE_SELF).ru_maxrss (pico de RSS — kB no Linux, bytes no macOS) ou VmRSS em /proc/self/status para o valor corrente. sys.getsizeof(obj) mede um objeto individual e serve para comparar estruturas.
Como saber o uso de CPU em Python?
Para o próprio processo, some ru_utime e ru_stime de resource.getrusage(resource.RUSAGE_SELF) e compare com o tempo decorrido medido por time.monotonic(): a razão entre os dois é a fração de um núcleo que você está consumindo. Para o percentual de CPU da máquina inteira, a biblioteca padrão não fornece: instale psutil e use psutil.cpu_percent(interval=1). Para contar núcleos, prefira len(os.sched_getaffinity(0)) — reflete o limite real em contêineres — em vez de os.cpu_count().
Qual a diferença entre sys.getsizeof, tracemalloc e ru_maxrss?
sys.getsizeof(objeto) devolve o tamanho em bytes de um único objeto, sem somar os objetos que ele referencia; é útil para comparar estruturas entre si. tracemalloc rastreia em execução as alocações feitas pelo código Python e associa cada bloco à linha que o criou, sendo a ferramenta para localizar vazamentos. ru_maxrss, do resource.getrusage, é o pico de memória do processo inteiro medido pelo sistema operacional — inclui interpretador e extensões nativas e é o número que importa quando o risco é o contêiner ser morto por OOM.
Como limitar o uso de CPU de um processo Python?
No Linux, os.sched_setaffinity(0, {0, 1}) restringe o processo aos núcleos listados e os.nice(10) reduz a prioridade de escalonamento, fazendo o processo ceder CPU a outros. Para limitar a uma porcentagem exata, a saída da stdlib é alternar signal.SIGSTOP/SIGCONT em janelas de tempo; a solução recomendada em produção é aplicar CPUQuota= no systemd ou cgroups no nível do contêiner. Aumentar threads ou processos não limita CPU — aumenta o consumo.
Como listar e finalizar processos filhos em Python?
Com subprocess.Popen(["python", "script.py"]), você obtém o PID em processo.pid, verifica o estado sem bloquear com processo.poll() e recolhe o status final com processo.wait() ou processo.communicate(timeout=...) — o que também evita processos zumbis no Linux. Para encerrar, use processo.terminate() (SIGTERM), aguarde o timeout e só então processo.kill() (SIGKILL). Filhos criados fora do subprocess são esperados com os.waitpid(pid, 0), e um PID arbitrário recebe sinal com os.kill(pid, signal.SIGTERM) — confirmando antes em /proc/<pid>/cmdline, porque PIDs se reciclam.
Conclusão
sys, os e resource formam um kit de monitoramento completo para o próprio processo, sem nenhuma dependência: getsizeof compara estruturas, tracemalloc aponta a linha que aloca, getrusage e /proc medem RSS e CPU reais, sched_getaffinity diz quantos núcleos você de fato tem, e subprocess/os.kill administram processos filhos do início ao fim. Quando a pergunta escala para a máquina inteira — histórico, discos, rede, percentual global — o psutil assume, e em produção as métricas do seu serviço devem seguir o fluxo de observabilidade do seu deploy.
O próximo passo é transformar esses números em decisões: limite de RSS que recicla um worker, CPU% que justifica paralelismo, e logs estruturados que contam essa história quando ninguém está olhando. Dominar esse kit também rende pontos em processos seletivos — veja o que as empresas esperam nas vagas de Python.