> For the complete documentation index, see [llms.txt](https://docs.skail.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.skail.dev/construa/como-fazer/padroes-e-antipadroes/polling-de-sistema-externo.md).

# Polling de sistema externo

Consultar um sistema que não avisa quando termina: sem webhook, sem evento, só uma API de status.

O padrão é um laço (loop) com um command que consulta, um delay crescente entre consultas e um limite de tentativas, tudo durável.

## Quando usar

* Emissão em órgão público que "está processando".
* Relatório que o parceiro gera em minutos.
* Transferência que o banco confirma depois.
* Job em outro sistema.

Se o sistema oferece webhook, prefira [esperar um evento](/construa/como-fazer/disparar-um-evento-pela-api.md); polling é para quando não oferece, ou como plano B quando o webhook não chegou no prazo.

## O padrão

{% tabs %}
{% tab title="C#" %}

```csharp
public enum StatusEmissao { Processando, Autorizada, Rejeitada }
public record ConsultaEmissao(StatusEmissao Status, string? Protocolo, string? Motivo);

[SkailFunction]
public async SkailTask EmitirNotaNaPrefeitura(Guid notaId)
{
    var recibo = await EnviarLote(notaId);                          // command: inicia o processamento lá

    var intervalo = TimeSpan.FromSeconds(30);
    for (var tentativa = 1; tentativa <= 20; tentativa++)
    {
        await SkailTask.Delay(intervalo);

        var consulta = await ConsultarLote(recibo);                  // command: pergunta o status
        switch (consulta.Status)
        {
            case StatusEmissao.Autorizada:
                await RegistrarAutorizacao(notaId, consulta.Protocolo!);
                return;
            case StatusEmissao.Rejeitada:
                await RegistrarRejeicao(notaId, consulta.Motivo!);
                return;
        }

        intervalo = intervalo * 2 < TimeSpan.FromMinutes(10) ? intervalo * 2 : TimeSpan.FromMinutes(10);
    }

    await MarcarParaAnaliseManual(notaId, recibo);                   // esgotou: alguém precisa olhar
}

[SkailCommand(retryCount: 5)]
public async SkailTask<string> EnviarLote(Guid notaId) { /* HTTP; idempotente pelo notaId */ }

[SkailCommand(retryCount: 3)]
public async SkailTask<ConsultaEmissao> ConsultarLote(string recibo) { /* HTTP GET */ }
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
import { Skail, SkailCommand, SkailFunction } from "@skail/runtime";

export type StatusEmissao = "Processando" | "Autorizada" | "Rejeitada";

export interface ConsultaEmissao {
  Status: StatusEmissao;
  Protocolo: string | null;
  Motivo: string | null;
}

const MINUTO = 60_000;

export class NotaService {
  @SkailFunction()
  async EmitirNotaNaPrefeitura(notaId: string): Promise<void> {
    const recibo = await this.EnviarLote(notaId); // command: inicia o processamento lá

    let intervalo = 30_000;
    for (let tentativa = 1; tentativa <= 20; tentativa++) {
      await Skail.delay(intervalo);

      const consulta = await this.ConsultarLote(recibo); // command: pergunta o status
      switch (consulta.Status) {
        case "Autorizada":
          await this.RegistrarAutorizacao(notaId, consulta.Protocolo!);
          return;
        case "Rejeitada":
          await this.RegistrarRejeicao(notaId, consulta.Motivo!);
          return;
      }

      intervalo = Math.min(intervalo * 2, 10 * MINUTO);
    }

    await this.MarcarParaAnaliseManual(notaId, recibo); // esgotou: alguém precisa olhar
  }

  @SkailCommand({ retryCount: 5 })
  async EnviarLote(notaId: string): Promise<string> {
    /* HTTP; idempotente pelo notaId */
    return notaId;
  }

  @SkailCommand({ retryCount: 3 })
  async ConsultarLote(recibo: string): Promise<ConsultaEmissao> {
    /* HTTP GET */
    return { Status: "Processando", Protocolo: null, Motivo: null };
  }

  @SkailCommand() async RegistrarAutorizacao(notaId: string, protocolo: string): Promise<void> {}
  @SkailCommand() async RegistrarRejeicao(notaId: string, motivo: string): Promise<void> {}
  @SkailCommand() async MarcarParaAnaliseManual(notaId: string, recibo: string): Promise<void> {}
}
```

{% endtab %}
{% endtabs %}

Este código garante o seguinte:

* Cada consulta é um command; se a API de status falhar, o runtime retenta a consulta (3 vezes) antes de a exceção chegar à function.
* Cada delay hiberna a execução, que não consome recursos entre uma consulta e outra.
* O contador `tentativa` e o `intervalo` são variáveis locais, reconstruídas no replay a partir dos mesmos passos, então o laço (loop) é determinístico.
* O limite de 20 tentativas com intervalo crescente até 10 minutos cobre cerca de 2 horas e 45 minutos de espera (30 s, 1, 2, 4 e 8 minutos nas cinco primeiras e 10 minutos em cada uma das outras 15); ajuste ao sistema.
* Quando esgota, a decisão vai para uma pessoa, registrada como command para aparecer no Monitor.

## Backoff

O intervalo dobra a cada consulta até um teto. Isso poupa o sistema externo quando ele está lento e responde rápido quando ele é rápido. O cálculo é aritmética sobre variáveis locais: pode ficar na function. O que não pode é adicionar jitter com um número aleatório na function; se quiser jitter, gere-o no command de consulta e devolva junto, ou derive-o deterministicamente do `notaId`.

## Polling com prazo total

Em vez de contar tentativas, limite por tempo total combinando com uma espera:

{% tabs %}
{% tab title="C#" %}

```csharp
var prazoTotal = SkailTask.Delay(TimeSpan.FromHours(3));
var intervalo  = TimeSpan.FromSeconds(30);

while (true)
{
    var espera = SkailTask.Delay(intervalo);
    if (await SkailTask.WhenAny(espera, prazoTotal) == prazoTotal) { await MarcarParaAnaliseManual(notaId, recibo); return; }

    var consulta = await ConsultarLote(recibo);
    if (consulta.Status != StatusEmissao.Processando) { /* trata e return */ }

    intervalo = intervalo * 2 < TimeSpan.FromMinutes(10) ? intervalo * 2 : TimeSpan.FromMinutes(10);
}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
import { Skail, SkailCommand, SkailFunction } from "@skail/runtime";

export interface ConsultaEmissao {
  Status: "Processando" | "Autorizada" | "Rejeitada";
}

const MINUTO = 60_000;

export class NotaService {
  @SkailFunction()
  async EmitirNotaComPrazoTotal(notaId: string, recibo: string): Promise<void> {
    const prazoTotal = Skail.delay(3 * 60 * MINUTO);
    let intervalo = 30_000;

    while (true) {
      const intervaloPassou = await Skail.whenAny(this.AguardarIntervalo(intervalo), prazoTotal);
      if (intervaloPassou === undefined) {
        await this.MarcarParaAnaliseManual(notaId, recibo);
        return;
      }

      const consulta = await this.ConsultarLote(recibo);
      if (consulta.Status !== "Processando") {
        /* trata e return */
        return;
      }

      intervalo = Math.min(intervalo * 2, 10 * MINUTO);
    }
  }

  @SkailFunction()
  async AguardarIntervalo(intervalo: number): Promise<boolean> {
    await Skail.delay(intervalo);
    return true;
  }

  @SkailCommand() async ConsultarLote(recibo: string): Promise<ConsultaEmissao> { return { Status: "Processando" }; }
  @SkailCommand() async MarcarParaAnaliseManual(notaId: string, recibo: string): Promise<void> {}
}
```

{% endtab %}
{% endtabs %}

Em TypeScript, o delay resolve `undefined` e o `Skail.whenAny` devolve só o valor de quem terminou primeiro, então dois delays no mesmo `Skail.whenAny` não dizem qual venceu. Por isso a espera de cada intervalo fica na function filha `AguardarIntervalo`, que devolve `true`: `undefined` quer dizer que o prazo total venceu. A consulta fica fora da corrida, como no C#, e o prazo nunca interrompe uma consulta em andamento.

## Webhook com polling de segurança

O parceiro tem webhook, mas às vezes ele não chega. Espere o evento com prazo; no timeout, consulte por polling antes de declarar falha:

{% tabs %}
{% tab title="C#" %}

```csharp
var confirmacao = SkailTask.WaitForEvent<Confirmacao>(Eventos.LoteProcessado, recibo);
var prazo       = SkailTask.Delay(TimeSpan.FromMinutes(15));

if (await SkailTask.WhenAny(confirmacao, prazo) == confirmacao)
{
    await Tratar(await confirmacao);
    return;
}

var consulta = await ConsultarLote(recibo);                         // o webhook não veio: pergunta
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
import { Skail, SkailCommand, SkailFunction } from "@skail/runtime";

const Eventos = { LoteProcessado: "LOTE_PROCESSADO" } as const;

export interface Confirmacao {
  Protocolo: string;
}

export interface ConsultaEmissao {
  Status: "Processando" | "Autorizada" | "Rejeitada";
}

export class NotaService {
  @SkailFunction()
  async AguardarLote(recibo: string): Promise<void> {
    const confirmacao = Skail.waitForEvent<Confirmacao>(Eventos.LoteProcessado, recibo);
    const prazo = Skail.delayMinutes(15);

    const resultado = await Skail.whenAny(confirmacao, prazo);
    if (resultado !== undefined) {
      await this.Tratar(resultado);
      return;
    }

    const consulta = await this.ConsultarLote(recibo); // o webhook não veio: pergunta
    void consulta;
  }

  @SkailCommand() async Tratar(confirmacao: Confirmacao): Promise<void> {}
  @SkailCommand() async ConsultarLote(recibo: string): Promise<ConsultaEmissao> { return { Status: "Processando" }; }
}
```

{% endtab %}
{% endtabs %}

## O que não fazer

* Consultar em laço (loop) dentro de um command com pausas comuns da linguagem (ocupa a instância da aplicação por horas e um único command fica sem checkpoint).
* Polling sem limite (histórico infinito, ver [Antipadrões](/construa/como-fazer/padroes-e-antipadroes/antipadroes-o-que-nao-fazer-e-por-que.md)).
* Intervalo fixo de poucos segundos por horas (passos demais, pressão no sistema externo).

## Próximos passos

<table data-view="cards"><thead><tr><th></th><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><i class="fa-stopwatch">:stopwatch:</i></td><td><strong>Timeout e prazo</strong></td><td>Para as variações de prazo.</td><td><a href="/construa/como-fazer/padroes-e-antipadroes/timeout-e-prazo.md">Timeout e prazo</a></td></tr><tr><td><i class="fa-clock">:clock:</i></td><td><strong>Tempo e agendamento</strong></td><td>Para o delay.</td><td><a href="/construa/conceitos/tempo-e-agendamento.md">Tempo e agendamento</a></td></tr><tr><td><i class="fa-receipt">:receipt:</i></td><td><strong>Emissão de NFS-e</strong></td><td>É um exemplo completo com processamento assíncrono no órgão.</td><td><a href="/construa/casos-de-uso/emissao-de-nota-fiscal.md">Emissão de nota fiscal</a></td></tr></tbody></table>
