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.

05 Oct 2026 13 min de leitura Equipe Python Dev BR

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 saberFerramenta da stdlibObservação
Tamanho de um objeto (dict, list, classe)sys.getsizeof(obj)Só o objeto em si, em bytes
Memória alocada pelo Python, por linhatracemallocMostra a linha de código responsável
Pico de memória RSS do processoresource.getrusage().ru_maxrssMedido pelo sistema operacional
Memória atual do processo no Linux/proc/self/status (via open()/pathlib)Ler VmRSS em bytes
CPU consumida pelo processogetrusage().ru_utime + ru_stimeDivida pelo tempo decorrido
Núcleos disponíveis ao processoos.sched_getaffinity(0) / os.cpu_count()O affinity é o que vale sob contêiner
Percentual de CPU da máquinafora da stdlib: psutil ou /proc/statPadrão de mercado: psutil
Gerenciar processos e filhossubprocess, os.kill, os.waitpidPopen.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:

  1. getsizeof nã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.
  2. 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 psutil onde 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 Popen e nunca chamar wait/communicate. Gera processos zumbis e vazamento de descritores em loops longos.
  • Chamar os.kill(pid, SIGKILL) direto. Sem SIGTERM + timeout você perde a chance de o processo finalizar arquivos e logs em paz.
  • Deixar tracemalloc ligado 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: getrusage para CPU e pico de RSS (/proc/self/status para 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.

E
Equipe Python Dev BR

Contribuidor do Python Dev BR

Artigos relacionados