---
title: "sys e os em Python: monitorar memória, CPU e processos"
url: "https://python.dev.br/blog/python-sys-os-memoria-cpu-processos/"
markdown_url: "https://python.dev.br/blog/python-sys-os-memoria-cpu-processos.MD"
description: "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."
date: "2026-10-05"
author: "Equipe Python Dev BR"
---

# 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](/blog/python-subprocess-comandos-externos/), [logging em Python](/blog/logging-em-python/) e [ETL com Python](/blog/etl-python-2026/).

## 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:

```python
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](/blog/etl-python-2026/).

## 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**:

```python
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](/blog/python-csv-leitura-escrita-arquivos/).

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:

```python
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](/blog/apscheduler-agendar-tarefas-python/).

## 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:

```python
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`:

```python
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](/blog/logging-em-python/) 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:

```python
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](/blog/python-statistics-media-mediana-desvio-padrao/) faz ao trocar cálculos manuais por `statistics`), vetorize com [NumPy](/blog/introducao-ao-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:

```python
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:

```python
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:

```python
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](/blog/python-threading-threadpoolexecutor/) 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:

```python
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**:

```python
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`:

```python
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:

```python
"""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](/blog/python-e-docker/).

## 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](/blog/logging-em-python/)

## 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](/blog/deploy-aplicacao-python/).

O próximo passo é transformar esses números em decisões: limite de RSS que recicla um worker, CPU% que justifica [paralelismo](/blog/python-threading-threadpoolexecutor/), 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](/vagas/).
