O session_start() falha com "headers already sent" porque a sessão precisa mandar um cabeçalho HTTP (o Set-Cookie com o ID da sessão), e cabeçalho só pode ser enviado antes do corpo da resposta. Se qualquer coisa já foi impressa — um echo, uma linha de HTML, ou até um espaço em branco invisível antes da tag <?php — o PHP já despachou os cabeçalhos e fechou essa janela. Não dá para voltar atrás. A regra que resolve: session_start() vai na primeira linha executável do script, antes de qualquer saída. Abaixo, por que o HTTP funciona assim, como descobrir exatamente qual linha imprimiu antes da hora, e as quatro causas que respondem por praticamente todos os casos.
O mecanismo: como uma resposta HTTP é montada
Toda resposta HTTP tem duas partes, nessa ordem obrigatória: primeiro os cabeçalhos (metadados — tipo do conteúdo, cookies, redirecionamentos), depois uma linha em branco, e então o corpo (o HTML que o usuário vê). A linha em branco é o separador: tudo que vem depois dela é conteúdo, e o protocolo não permite voltar e inserir mais cabeçalho.
O PHP acompanha esse fluxo. Ele acumula os cabeçalhos numa lista enquanto seu script roda e só os envia quando aparece o primeiro byte de saída. Nesse instante ele despacha os headers, manda a linha em branco e começa a transmitir o corpo. A partir daí, qualquer função que tente mexer em cabeçalho — header(), setcookie() e o session_start(), que internamente faz um setcookie — chega tarde demais.
É por isso que o erro não é "bug de sessão": é uma consequência direta do protocolo. E é por isso que a solução nunca é mexer na configuração da sessão, e sim na ordem do seu código.
1. O script começa — nada foi enviado
O PHP guarda os cabeçalhos numa lista interna. Nada saiu para o navegador ainda; a janela para adicionar headers está aberta.
<?php
// nenhuma saída até aqui — tudo certo
2. Aparece a primeira saída
Um echo, um trecho de HTML fora das tags PHP, um espaço solto. O PHP entende: "acabou o momento dos cabeçalhos".
echo "Bem-vindo!"; // ← primeiro byte do CORPO
3. Os cabeçalhos são despachados e trancados
O PHP envia a lista de headers, manda a linha em branco separadora e passa a transmitir o corpo. A janela se fechou.
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
← linha em branco: daqui pra frente é corpo
Bem-vindo!
4. session_start() chega atrasado
Ele tenta enviar Set-Cookie: PHPSESSID=..., mas não há mais onde encaixar. O PHP emite o aviso e a sessão não inicia.
Warning: session_start(): Session cannot be started after
headers have already been sent
5. A consequência silenciosa
Como a sessão não iniciou, o $_SESSION vira um array comum que morre no fim do request. É por isso que o login "não gruda" e o usuário volta deslogado na próxima página — mesmo o código de login parecendo correto.
$_SESSION['usuario_id'] = 42; // escreve… e se perde
Diagnóstico: achar a linha culpada
Aqui está a parte que economiza horas. O aviso do session_start() diz que houve saída antes, mas não diz onde — e adivinhar em um projeto com dezenas de includes é sofrimento. Duas formas de descobrir o ponto exato:
1. Provoque a mensagem completa. Diferente do session_start(), as funções header() e setcookie() informam o local da saída original. Coloque um header() qualquer logo antes e leia o aviso:
header('X-Debug: 1');
// Warning: Cannot modify header information - headers already sent by
// (output started at /var/www/config.php:2) in /var/www/index.php on line 3
O trecho output started at /var/www/config.php:2 é a resposta: o arquivo e a linha que imprimiram primeiro. Vá lá e remova aquela saída.
2. Pergunte ao próprio PHP. A função headers_sent() aceita duas variáveis por referência e as preenche com arquivo e linha:
if (headers_sent($arquivo, $linha)) {
die("Saída começou em: $arquivo na linha $linha");
}
session_start();
Ponha isso no topo, rode uma vez, anote o arquivo e a linha — e depois remova. Em minutos você tem o culpado, sem chutar.
As quatro causas, em ordem de frequência
1. Espaço ou linha em branco antes da tag de abertura. A causa nº 1 e a mais invisível.
2. BOM no início do arquivo. O Byte Order Mark é uma marca invisível de 3 bytes (EF BB BF) que alguns editores gravam no começo de arquivos UTF-8. Ela vem antes do <?php, o PHP a trata como saída comum e você recebe o erro — sem nenhum espaço visível no arquivo. A correção é salvar como "UTF-8 sem BOM" (no VS Code: escolha "UTF-8" em vez de "UTF-8 with BOM" na barra inferior).
3. Espaço depois da tag de fechamento. Mesma armadilha, do outro lado: se um arquivo incluído termina com ?> seguido de uma quebra de linha, esse \n é impresso quando o arquivo é incluído.
4. Saída deliberada antes da hora. Um echo de depuração esquecido, um print_r(), um var_dump(), ou HTML no topo do arquivo antes do bloco de lógica. Aqui o conserto é só reordenar:
<!DOCTYPE html>
<html>
<body>
<?php
session_start(); // tarde demais: o HTML acima já foi enviado
$_SESSION['visitas'] = 1;
?><?php
session_start(); // primeira linha executável, antes de tudo
$_SESSION['visitas'] = 1;
?>
<!DOCTYPE html>
<html>
<body>Seu index.php começa exatamente com <?php session_start(); na linha 1, mas o erro continua. Qual é a explicação mais provável?
Ver resposta
Resposta certa: Um arquivo incluído antes imprime algo (espaço antes de <?php, BOM ou \n depois de ?>)
A saída não precisa vir do arquivo principal. Se index.php faz require 'config.php' antes, qualquer byte impresso por config.php já fecha os cabeçalhos. Use headers_sent($arquivo, $linha) — ele aponta o arquivo real, que quase sempre é um include, não o script que você está olhando.
E o ob_start()? (o paliativo honesto)
Você vai encontrar conselhos mandando pôr ob_start() no topo. Funciona: o output buffering segura a saída numa memória intermediária em vez de mandar na hora, então os cabeçalhos continuam abertos e o session_start() passa. Mas entenda o que você está fazendo: isso esconde o sintoma, não corrige a causa. O espaço perdido ou o echo fora de lugar continuam lá, e você passa a depender de um buffer para que a aplicação funcione — inclusive em produção, onde uma configuração diferente pode desligar esse comportamento. Use ob_start() conscientemente (é uma ferramenta legítima para capturar saída), nunca como esparadrapo para não procurar a linha culpada.
Resumo de decisão
| Sintoma / causa | Como confirmar | Correção |
|---|---|---|
Espaço/linha antes de <?php |
head -c 20 arquivo.php | xxd |
Apagar tudo antes da tag |
| BOM no início | Os bytes EF BB BF antes de <?php |
Salvar como UTF-8 sem BOM |
Espaço depois de ?> |
Última linha do include | Omitir o ?> em arquivo só-PHP |
echo/HTML antes da lógica |
headers_sent($f, $l) aponta o local |
Mover session_start() para o topo |
| Não sei onde começou | header('X-Debug: 1') → lê output started at |
Ir ao arquivo:linha indicado |
Em uma frase: cabeçalho vem antes de corpo, sempre — então session_start() (e header(), e setcookie()) precisa acontecer antes do primeiro byte impresso, incluindo os bytes invisíveis.
A seção de Sessões do manual cobre o ciclo completo — como o ID viaja no cookie, o que a configuração controla e os cuidados de segurança. Entender o cookie explica por que a ordem importa tanto.
O exemplo básico mostra a estrutura correta de uma página com sessão: session_start() no topo absoluto, depois a lógica, depois a saída. É o padrão que evita esse erro de nascença.