Como Usar ActiveJob para Processar Consultas de CPF em Segundo Plano no Rails

Aprenda a usar ActiveJob no Rails para processar consultas de CPF em segundo plano, com Sidekiq, retry automático e callbacks de conclusão.

Redação CPFHub.io
Redação CPFHub.io
··7 min de leitura
Como Usar ActiveJob para Processar Consultas de CPF em Segundo Plano no Rails

Para processar consultas de CPF em segundo plano no Rails sem bloquear requisições HTTP, crie um ConsultarCpfJob com ActiveJob e Sidekiq: o controller responde imediatamente com status 202, o job consulta a API da CPFHub.io em background com retry automático e persiste o resultado no banco — suportando desde validações individuais no checkout até lotes de milhares de CPFs com filas priorizadas.

Introdução

Em aplicações Rails, processar consultas de CPF de forma síncrona durante uma requisição HTTP pode causar timeouts e degradar a experiência do usuário. O ActiveJob, framework nativo do Rails para processamento em background, oferece uma abstração elegante que funciona com diversos backends como Sidekiq, Resque e Delayed Job.

Configuração do ActiveJob com Sidekiq

Primeiro, configure o Rails para utilizar o Sidekiq como backend do ActiveJob.

# Gemfile
gem "sidekiq", "~> 7.0"
gem "faraday", "~> 2.0"
gem "faraday-retry", "~> 2.0"

# config/application.rb
config.active_job.queue_adapter = :sidekiq

# config/sidekiq.yml
:concurrency: 10
:queues:
    - [consulta_cpf_urgente, 3]
    - [consulta_cpf_padrao, 2]
    - [consulta_cpf_lote, 1]
FilaPrioridadeUso
consulta_cpf_urgenteAlta (3)Checkout em tempo real
consulta_cpf_padraoMédia (2)Cadastros e validações regulares
consulta_cpf_loteBaixa (1)Processamento em massa

Criando o job de consulta de CPF

O job encapsula toda a lógica de consulta à API, tratamento de erros e persistência do resultado.

# app/jobs/consultar_cpf_job.rb
class ConsultarCpfJob < ApplicationJob
    queue_as :consulta_cpf_padrao

    retry_on Faraday::TimeoutError, wait: :exponentially_longer, attempts: 5
    retry_on Faraday::ConnectionFailed, wait: 30.seconds, attempts: 3
    discard_on ActiveJob::DeserializationError

    before_perform :registrar_inicio
    after_perform :registrar_conclusao

    def perform(cpf, callback_class: nil, callback_id: nil)
    cliente_api = criar_cliente_api
    resposta = cliente_api.get("/cpf/#{cpf}")

    resultado = JSON.parse(resposta.body)

    if resultado["success"]
    salvar_resultado(cpf, resultado["data"])
    executar_callback(callback_class, callback_id, resultado["data"])
    else
    registrar_falha(cpf, "CPF nao encontrado na base")
    end
    rescue Faraday::ClientError => e
    registrar_falha(cpf, "Erro na API: #{e.message}")
    raise
    end

    private

    def criar_cliente_api
    Faraday.new(url: "https://api.cpfhub.io") do |conn|
    conn.request :retry, max: 2, interval: 1, backoff_factor: 2
    conn.headers["x-api-key"] = ENV["CPFHUB_API_KEY"]
    conn.headers["Content-Type"] = "application/json"
    conn.options.timeout = 10
    conn.options.open_timeout = 5
    conn.adapter Faraday.default_adapter
    end
    end

    def salvar_resultado(cpf, dados)
    ConsultaCpf.create!(
    cpf: cpf,
    nome: dados["name"],
    genero: dados["gender"],
    data_nascimento: dados["birthDate"],
    status: "sucesso",
    consultado_em: Time.current
    )
    end

    def executar_callback(callback_class, callback_id, dados)
    return unless callback_class && callback_id

    klass = callback_class.constantize
    registro = klass.find(callback_id)
    registro.cpf_validado!(dados)
    end

    def registrar_inicio
    Rails.logger.info("[ConsultarCpfJob] Iniciando consulta")
    end

    def registrar_conclusao
    Rails.logger.info("[ConsultarCpfJob] Consulta concluida")
    end

    def registrar_falha(cpf, motivo)
    ConsultaCpf.create!(
    cpf: cpf,
    status: "falha",
    motivo_falha: motivo,
    consultado_em: Time.current
    )
    end
end

Enfileirando jobs a partir de controllers

O controller recebe a requisição e enfileira o job, respondendo imediatamente ao cliente.

# app/controllers/api/cpf_controller.rb
module Api
    class CpfController < ApplicationController
    def consultar
    cpf = params[:cpf].gsub(/\D/, "")

    unless cpf_valido?(cpf)
    render json: { erro: "CPF invalido" }, status: :unprocessable_entity
    return
    end

    # Verificar se já existe no cache
    consulta_existente = ConsultaCpf.recente.find_by(cpf: cpf)
    if consulta_existente&.sucesso?
    render json: {
    fonte: "cache",
    dados: consulta_existente.as_json
    }
    return
    end

    # Enfileirar para processamento em background
    job = ConsultarCpfJob.perform_later(cpf)

    render json: {
    mensagem: "Consulta enfileirada com sucesso",
    job_id: job.provider_job_id,
    status: "pendente"
    }, status: :accepted
    end

    def consultar_lote
    cpfs = params[:cpfs]

    cpfs.each_with_index do |cpf, index|
    ConsultarCpfJob
    .set(queue: :consulta_cpf_lote, wait: index.seconds)
    .perform_later(cpf.gsub(/\D/, ""))
    end

    render json: {
    mensagem: "#{cpfs.size} CPFs enfileirados para processamento",
    status: "pendente"
    }, status: :accepted
    end

    private

    def cpf_valido?(cpf)
    cpf.match?(/\A\d{11}\z/) && !cpf.match?(/\A(\d)\1{10}\z/)
    end
    end
end

Model e migration para armazenar resultados

Crie o model e a migration para persistir os resultados das consultas.

# db/migrate/XXXXXX_create_consultas_cpf.rb
class CreateConsultasCpf < ActiveRecord::Migration[7.1]
    def change
    create_table :consultas_cpf do |t|
    t.string :cpf, null: false, index: true
    t.string :nome
    t.string :genero
    t.date :data_nascimento
    t.string :status, null: false, default: "pendente"
    t.text :motivo_falha
    t.datetime :consultado_em

    t.timestamps
    end

    add_index :consultas_cpf, [:cpf, :created_at]
    add_index :consultas_cpf, :status
    end
end

# app/models/consulta_cpf.rb
class ConsultaCpf < ApplicationRecord
    validates :cpf, presence: true
    validates :status, inclusion: {
    in: %w[pendente sucesso falha]
    }

    scope :recente, -> { where("created_at > ?", 24.hours.ago) }
    scope :sucesso, -> { where(status: "sucesso") }
    scope :falha, -> { where(status: "falha") }

    def sucesso?
    status == "sucesso"
    end
end
CampoTipoDescrição
cpfstringCPF consultado (11 dígitos)
nomestringNome retornado pela API
generostringGênero retornado pela API
data_nascimentodateData de nascimento retornada
statusstringpendente, sucesso ou falha
motivo_falhatextMotivo em caso de falha
consultado_emdatetimeMomento da consulta à API

Monitoramento e métricas

Monitore os jobs para garantir que o processamento está saudável.

# app/services/metricas_cpf_service.rb
class MetricasCpfService
    def self.resumo
    {
    total_consultas: ConsultaCpf.count,
    sucessos: ConsultaCpf.sucesso.count,
    falhas: ConsultaCpf.falha.count,
    taxa_sucesso: calcular_taxa_sucesso,
    filas: {
    urgente: Sidekiq::Queue.new("consulta_cpf_urgente").size,
    padrao: Sidekiq::Queue.new("consulta_cpf_padrao").size,
    lote: Sidekiq::Queue.new("consulta_cpf_lote").size
    },
    workers_ativos: Sidekiq::Workers.new.size
    }
    end

    def self.calcular_taxa_sucesso
    total = ConsultaCpf.count
    return 0 if total.zero?

    ((ConsultaCpf.sucesso.count.to_f / total) * 100).round(2)
    end
end

Perguntas frequentes

Por que usar ActiveJob em vez de processar consultas de CPF de forma síncrona no controller?

Consultas síncronas à API externa dentro de uma requisição HTTP podem demorar até 1-2 segundos em condições normais — e muito mais em picos ou instabilidade de rede. Com ActiveJob e Sidekiq, o controller responde em milissegundos e o processamento real ocorre no worker em background, sem afetar o tempo de resposta da aplicação para o usuário.

Como configurar o número de workers Sidekiq para processar consultas de CPF?

Defina concurrency no sidekiq.yml de acordo com o volume esperado. Para o plano Pro da CPFHub.io (1.000 consultas/mês), uma concorrência de 5 a 10 workers já é suficiente. Se o volume ultrapassar o limite do plano, a API não bloqueia — cobra R$0,15 por consulta adicional. Escale os workers conforme o volume crescer.

Como garantir conformidade com a LGPD ao armazenar resultados de consultas de CPF no banco?

Armazene apenas os campos necessários para a finalidade declarada, não o CPF em texto puro se um identificador interno bastar, implemente controle de acesso às tabelas de consulta e defina política de retenção com exclusão automática após o prazo necessário. A ANPD orienta que o princípio da necessidade se aplica tanto à coleta quanto ao armazenamento de dados pessoais.

O que acontece se o Sidekiq reiniciar durante o processamento de um job de CPF?

O Sidekiq usa o Redis para rastrear jobs em andamento. Se um worker for interrompido durante o processamento, o job retorna à fila após o timeout de visibilidade e é retentado automaticamente conforme a configuração de retry_on no job. Para evitar registros duplicados no banco, verifique a existência do CPF antes de salvar (upsert ou find_or_create_by).


Conclusão

O ActiveJob com Sidekiq oferece uma solução elegante e robusta para processar consultas de CPF em segundo plano no Rails. Com retry automático, filas priorizadas e persistência de resultados, o sistema garante que nenhuma consulta seja perdida e que a aplicação responda rapidamente ao usuário. A separação entre produtores e consumidores permite escalar o processamento de forma independente da aplicação web.

Cadastre-se em cpfhub.io — 50 consultas mensais gratuitas, sem cartão de crédito — e comece a processar consultas de CPF em background na sua aplicação Rails hoje mesmo.

CPFHub.io

Pronto para integrar a API?

50 consultas gratuitas para testar agora. Sem cartão de crédito. Acesso imediato à documentação.

Redação CPFHub.io

Sobre a redação

Redação CPFHub.io

Time editorial especializado em APIs de CPF, identidade digital e compliance no mercado brasileiro. Produzimos guias técnicos, análises regulatórias e tutoriais sobre LGPD e KYC para desenvolvedores e líderes de produto.

WhatsAppFale conosco via WhatsApp