GUC max_parallel_apply_workers_per_subscription revela os limites do apply paralelo no PostgreSQL
O parâmetro que promete replicação lógica paralela na verdade controla quantas transações grandes podem ser aplicadas ao mesmo tempo, não a velocidade de cada uma; entender essa diferença evita configuração inútil em produção.

O nome do parâmetro sugere paralelismo generalizado na replicação lógica, e é fácil supor que aumentá-lo acelere um subscriber atrasado. Um artigo recente da série "All Your GUCs in a Row", publicado no Planet PostgreSQL↳PostgreSQL11 conteúdosPostgreSQL via SSL com GolangData · abr 20195 itens legais sobre data types do PostgreSQLData · mar 20195 serviços gratuitos na cloud para bancos de dados PostgresData · fev 2025Ver tudo em Data →, desmonta essa expectativa com testes práticos em 18.6 e 19 beta 3. O que max_parallel_apply_workers_per_subscription controla é bem mais estreito: quantas transações grandes e ainda abertas uma assinatura pode aplicar simultaneamente. Toda transação além desse limite é gravada em arquivo e aplicada depois, em série, pelo mesmo processo que já está enfileirando o resto.
Para quem administra ambientes com múltiplos subscribers em produção, essa distinção é o que separa um tuning eficaz de um ajuste que não muda nada no gráfico de lag.
O que o parâmetro efetivamente faz
O GUC chegou no PostgreSQL 16 junto com a opção streaming = parallel, valor default 2, contexto sighup (aceita reload, sem exigir restart), faixa de 0 a 1024. Nas versões 16 e 17 era preciso pedir streaming = parallel explicitamente na assinatura; no PostgreSQL 18 esse modo passou a ser o padrão de CREATE SUBSCRIPTION. Na prática, isso significa que qualquer assinatura criada num 18 sem configuração explícita já nasce sujeita a este limite, sem que o DBA tenha pedido. Assinaturas que chegam via pg_upgrade mantêm o comportamento antigo, e o pg_dump do 18 escreve streaming = off de forma explícita para garantir isso na restauração.
O mecanismo por baixo: quando o reorder buffer do publisher ultrapassa logical_decoding_work_mem, ele começa a enviar ao subscriber a maior transação aberta antes mesmo dela comitar. No subscriber, o leader apply worker entrega cada transação streamada a um parallel apply worker, que vai aplicando as mudanças conforme chegam. Uma transação, um worker. Aumentar o parâmetro não faz uma única transação aplicar mais rápido; e tudo que não é streamado (com o limiar padrão de 64MB, praticamente todo o tráfego de um sistema OLTP comum) continua sendo aplicado pelo leader, sozinho, em ordem de commit. Quando uma transação streamada finalmente comita, o leader espera o worker correspondente terminar antes de seguir, então a ordem de commit é preservada mesmo aqui. O ganho real é outro: quando o commit chega, a maior parte do trabalho pesado já foi feita.
O número que sustenta o argumento
O autor da publicação mediu isso diretamente: em 18.6, um insert de 400 mil linhas no publisher seguido de uma transação de uma linha, comitada imediatamente depois. Com um parallel apply worker disponível, a transação de uma linha ficou visível no subscriber cerca de 0,2 segundo após o commit da transação grande. Com o parâmetro em 0, o leader gravou 267MB em base/pgsql_tmp e só começou a aplicar no momento do commit; a transação de uma linha esperou entre 3,2 e 4,0 segundos. A máquina de teste era pequena e a transação também, mas a proporção entre os dois cenários (0,2s contra ~3,5s) é o dado concreto que justifica olhar para este GUC quando o publisher roda cargas em lote sobrepostas.
Esgotar o pool de workers é silencioso
A documentação diz que as mudanças vão para um worker paralelo "se disponível", e este parâmetro decide boa parte dessa disponibilidade. Com o padrão de 2, o autor manteve três transações grandes abertas ao mesmo tempo no publisher. A consulta a pg_stat_subscription mostrou dois workers parallel apply ativos e a terceira transação foi para um arquivo temporário de 133MB, sem nenhum aviso no nível de log padrão. Só com log_temp_files = 0 aparece um rastro, registrado quando o arquivo é removido, com STREAM COMMIT no contexto. Esse é o sinal a procurar: um arquivo temporário de um apply worker de replicação lógica com STREAM COMMIT no contexto indica uma transação streamada que não conseguiu worker paralelo.
Dois contadores ajudam a monitorar isso em produção: pg_stat_database.temp_bytes no subscriber (que soma esses mesmos arquivos) e stream_txns, em pg_stat_replication_slots no publisher, que informa quantas transações foram candidatas a streaming. O custo do estouro é maior que a transação que estourou: no mesmo teste em 19 beta 3, os dois workers paralelos ficaram parados por oito segundos esperando o leader terminar de reaplicar a terceira transação a partir do arquivo.
Há uma diferença entre estourar o limite deste parâmetro e estourar o pool geral. Quando o esgotamento vem do pool configurado por max_logical_replication_workers, o log é explícito: com este GUC em 4, o pool também em 4 (padrão) e quatro transações grandes abertas, a quarta foi para arquivo e o leader registrou "out of logical replication worker slots" com STREAM START no contexto. A lição prática de ordenação: este parâmetro aceita reload; o pool de onde ele retira workers exige restart. Suba o pool primeiro, o GUC depois.
Três condições que anulam o paralelismo sem avisar
Existem três situações em que toda transação streamada vai para arquivo, independente do valor configurado, e nenhuma delas é anunciada no log:
- Publisher em versão anterior a 16. O autor apontou uma assinatura padrão do 18.6 para um publisher 15.19:
pg_subscription.substreamficou emp, nenhum worker paralelo apareceu e o leader aplicou tudo sozinho. Esse é exatamente o cenário de migração de um primary 14 ou 15 via replicação lógica, então o comportamento padrão do servidor novo não vai se manifestar durante essa migração específica. - Alguma tabela da assinatura fora do estado
r. Enquanto uma tabela ainda está em sincronização inicial, ou depois de umALTER SUBSCRIPTION ... REFRESH PUBLICATIONque adicionou uma tabela nova, a assinatura inteira cai para aplicação serial, mesmo com workers parados e ociosos no pool. - Um
SKIPpendente na assinatura. UmALTER SUBSCRIPTION ... SKIPdesliga o apply paralelo até o skip ser consumido, porque o leader precisa da transação inteira em mãos para decidir se é ela que deve ser pulada. O autor cita esse ponto a partir da documentação, sem tê-lo testado diretamente.
Zero como valor válido, e a conta de workers mantidos
Workers que terminam são reaproveitados, e cada um mantido ocupa uma vaga no pool enquanto a assinatura roda. A quantidade mantida é a metade do valor configurado, arredondada para baixo: 2 mantém um, 3 mantém um, 4 mantém dois, e 1 não mantém nenhum. Em 1, cada transação grande paga o custo de iniciar um processo novo e uma fila de memória compartilhada de 16MB; no teste de tempo do próprio autor, a execução que precisou iniciar worker levou 0,9 segundo em vez de 0,2. É uma troca razoável num subscriber com dezenas de assinaturas competindo por um pool apertado, e uma péssima ideia em qualquer outro cenário.
Zero é uma configuração real, não um desligamento acidental: ela transforma streaming = parallel em streaming = on para todas as assinaturas do servidor, via reload, sem tocar em catálogo. O momento certo de usar isso é durante um conflito de replicação. Quando uma mudança falha dentro de um parallel apply worker, o log traz o ID da transação remota, mas sem LSN, o que impede usar ALTER SUBSCRIPTION ... SKIP diretamente. Zerar o parâmetro globalmente força o retry a rodar no leader a partir do arquivo de spool, e aí o contexto traz o LSN necessário para o SKIP funcionar. A rota documentada, e mais cirúrgica, é mudar a opção streaming da própria assinatura afetada, resolvendo uma sem impactar as demais.
O que fica de fora
Se o problema é lag causado por um volume alto de transações comuns, e não por transações grandes concorrentes, este parâmetro não resolve nada: até a versão 19, nada no core do PostgreSQL paraleliza esse caminho, e um único processo continua aplicando essas transações em ordem. A recomendação do autor, e que resiste bem a qualquer ambiente de produção com assinaturas múltiplas, é deixar o valor em 2 e só subir quando aparecerem arquivos temporários de apply worker com STREAM COMMIT no contexto, sinal de que o publisher roda mais de duas transações em lote simultâneas. E, ao subir, aumentar primeiro max_logical_replication_workers na mesma proporção para cada assinatura afetada, porque o pool não cresce por conta própria.
Fonte 1: Planet PostgreSQL (https://postgr.es/p/9vf)
Fonte: Planet PostgreSQL
Este artigo foi escrito por Roberto Diniz, colunista de banco de dados. Conteúdo produzido por agente de IA da redação iMasters, sob revisão editorial humana. Saiba como produzimos no expediente.













