---
title: "Cargo temporário no Discord: como o bot remove sozinho depois de X dias"
description: "Crie cargos temporários no Discord com discord.js v14: salve a data de expiração em um banco, varra a cada minuto e remova o cargo mesmo após reiniciar o bot."
url: "https://streethosting.com.br/guias/bots-discord/bot-discord-cargos-temporarios-expiracao"
category: "bots-discord"
slug: "bot-discord-cargos-temporarios-expiracao"
datePublished: "2026-11-13"
dateModified: "2026-11-13"
author: "Equipe StreetHosting"
difficulty: "intermediario"
language: "pt-BR"
keywords:
  - "cargo temporario discord bot"
  - "discord.js remover cargo automaticamente"
  - "bot discord cargo vip expira"
  - "discord bot role expiration"
  - "setTimeout discord bot limite"
---

# Cargos temporários no Discord: VIP de 30 dias que expira sozinho

Um VIP de 30 dias ou um castigo de 7 dias só funcionam se o cargo sair na hora certa, mesmo que o bot reinicie no meio do caminho. Veja como guardar a data de expiração e remover o cargo com segurança.

> **Resposta rápida**
>
> Ao dar o cargo, grave no banco a data de expiração (em milissegundos desde 1970). Um `setInterval` de 60 segundos consulta as linhas vencidas e chama `member.roles.remove()`. O bot precisa da permissão Manage Roles e de um cargo acima do que vai gerenciar. Como o prazo está no banco, reiniciar o bot não perde nada.

## Por que setTimeout sozinho falha

A primeira ideia de quase todo mundo é dar o cargo e agendar a remoção com um temporizador:

```
await member.roles.add(ID_CARGO_VIP);
setTimeout(() => member.roles.remove(ID_CARGO_VIP), 30 * 24 * 60 * 60 * 1000);
```

Funciona no teste de 10 segundos e falha em produção por dois motivos.

- **O temporizador mora na memória.** Um deploy, uma queda de energia, um erro não tratado ou o simples reinício do processo apagam todos os temporizadores. O membro fica com o VIP para sempre e ninguém percebe.
- **O limite do Node.** O atraso máximo de um `setTimeout` é 2147483647 milissegundos, cerca de 24,8 dias. Passou disso, o Node avisa com um `TimeoutOverflowWarning` e dispara quase imediatamente. Um VIP de 30 dias seria removido no mesmo instante em que foi dado.

A saída é separar as duas coisas: o prazo fica gravado em um banco, que sobrevive a qualquer reinício, e uma rotina curta confere se alguma linha venceu. Essa mesma ideia serve para lembretes, banimentos temporários e qualquer coisa agendada, como no guia de [agendar mensagens no Discord](https://streethosting.com.br/guias/bots-discord/bot-discord-agendar-mensagens-discord).

## Guardar a data de expiração no banco

Para um bot pequeno, o SQLite com `better-sqlite3` resolve: é um arquivo só, sem serviço para manter. Instale com `npm install better-sqlite3` e crie a tabela uma vez, na inicialização:

```
// db.js
const Database = require('better-sqlite3');

const db = new Database('dados.db');
db.pragma('journal_mode = WAL');

db.exec(`
  CREATE TABLE IF NOT EXISTS cargos_temporarios (
    guild_id  TEXT    NOT NULL,
    user_id   TEXT    NOT NULL,
    role_id   TEXT    NOT NULL,
    expira_em INTEGER NOT NULL,
    PRIMARY KEY (guild_id, user_id, role_id)
  );
  CREATE INDEX IF NOT EXISTS idx_expira_em ON cargos_temporarios (expira_em);
`);

module.exports = db;
```

Dois detalhes importam. A chave primária junta servidor, usuário e cargo, então o mesmo cargo não gera duas linhas para a mesma pessoa. E `expira_em` guarda um número (milissegundos desde 1970, o que `Date.now()` devolve). Número é simples de comparar e não depende de fuso horário.

Se o seu bot já usa MariaDB, a tabela é a mesma, trocando o tipo da data por `BIGINT`, os IDs por `VARCHAR(20)` (o MariaDB não aceita coluna TEXT em chave primária sem tamanho) e o `ON CONFLICT` por `ON DUPLICATE KEY UPDATE`. Não use `INT` comum: um inteiro de 32 bits não comporta milissegundos de hoje.

## Dar o cargo e registrar o prazo

A função abaixo concede o cargo e grava a expiração. Se a pessoa já tinha o mesmo cargo, o prazo é substituído, o que na prática renova o VIP:

```
// cargos.js
const db = require('./db');

const gravar = db.prepare(`
  INSERT INTO cargos_temporarios (guild_id, user_id, role_id, expira_em)
  VALUES (?, ?, ?, ?)
  ON CONFLICT (guild_id, user_id, role_id)
  DO UPDATE SET expira_em = excluded.expira_em
`);

async function darCargoTemporario(member, roleId, duracaoMs, motivo) {
  await member.roles.add(roleId, motivo);
  const expiraEm = Date.now() + duracaoMs;
  gravar.run(member.guild.id, member.id, roleId, expiraEm);
  return expiraEm;
}

module.exports = { darCargoTemporario };
```

A ordem importa: primeiro o Discord aceita o cargo, depois você grava. Se a chamada ao Discord falhar, nenhuma linha órfã fica no banco. Agora um comando `/vip` que usa a função (veja como registrar comandos em [criar bot Discord com slash commands](https://streethosting.com.br/guias/bots-discord/criar-bot-discord-slash-commands)):

```
const { SlashCommandBuilder, PermissionFlagsBits, MessageFlags, InteractionContextType } = require('discord.js');

const vip = new SlashCommandBuilder()
  .setName('vip')
  .setDescription('Dá o cargo VIP por alguns dias')
  .setDefaultMemberPermissions(PermissionFlagsBits.ManageRoles)
  .setContexts(InteractionContextType.Guild) // só em servidor
  .addUserOption((o) => o.setName('usuario').setDescription('Quem recebe').setRequired(true))
  .addIntegerOption((o) =>
    o.setName('dias').setDescription('Duração em dias').setMinValue(1).setMaxValue(365).setRequired(true)
  );

// no handler de interactionCreate
if (interaction.commandName === 'vip') {
  const alvo = interaction.options.getMember('usuario');
  const dias = interaction.options.getInteger('dias', true);
  const cargo = interaction.guild.roles.cache.get(process.env.ID_CARGO_VIP);

  if (!alvo || !cargo?.editable) {
    return interaction.reply({
      content: 'Não consigo gerenciar esse cargo. Confira a hierarquia.',
      flags: MessageFlags.Ephemeral,
    });
  }

  const expiraEm = await darCargoTemporario(alvo, cargo.id, dias * 24 * 60 * 60 * 1000, `VIP de ${dias} dias`);
  await interaction.reply(`${alvo} é VIP até <t:${Math.floor(expiraEm / 1000)}:f> (<t:${Math.floor(expiraEm / 1000)}:R>).`);
}
```

O formato `<t:segundos:R>` é um timestamp do Discord: cada pessoa vê a data no próprio fuso e a versão relativa (“em 30 dias”) se atualiza sozinha.

## A varredura que remove o cargo

Agora a metade que importa. Uma função busca tudo que já venceu, remove o cargo e apaga a linha. Rode uma vez ao ligar (para recuperar o que venceu enquanto o bot estava fora) e depois a cada minuto:

```
// varredura.js
const db = require('./db');

const buscarVencidos = db.prepare('SELECT * FROM cargos_temporarios WHERE expira_em <= ? LIMIT 200');
const apagar = db.prepare(
  'DELETE FROM cargos_temporarios WHERE guild_id = ? AND user_id = ? AND role_id = ?'
);

let rodando = false;

async function varrer(client) {
  if (rodando) return; // não deixa duas varreduras se sobreporem
  rodando = true;
  try {
    const vencidos = buscarVencidos.all(Date.now());
    for (const item of vencidos) {
      try {
        const guild = await client.guilds.fetch(item.guild_id);
        const member = await guild.members.fetch(item.user_id);
        await member.roles.remove(item.role_id, 'Cargo temporário expirou');
        apagar.run(item.guild_id, item.user_id, item.role_id);
      } catch (err) {
        // 10007 = membro não está mais no servidor, 10011 = cargo apagado, 10004 = servidor desconhecido
        if ([10007, 10011, 10004].includes(err.code)) {
          apagar.run(item.guild_id, item.user_id, item.role_id);
        } else {
          console.error('Falha ao remover cargo, tento de novo na próxima rodada:', err);
        }
      }
    }
  } finally {
    rodando = false;
  }
}

function iniciarVarredura(client) {
  varrer(client);
  setInterval(() => varrer(client), 60 * 1000);
}

module.exports = { iniciarVarredura };
```

E no arquivo principal, depois que o cliente estiver pronto:

```
const { Events } = require('discord.js');
const { iniciarVarredura } = require('./varredura');

client.once(Events.ClientReady, () => {
  iniciarVarredura(client);
});
```

O ponto de segurança está no `catch`: só apagamos a linha quando o cargo foi removido ou quando sabemos que não há mais o que remover. Um erro temporário (rede, rate limit, permissão que faltou por alguns minutos) deixa a linha no banco e a próxima rodada tenta de novo. O `LIMIT 200` evita que um acúmulo grande trave o processo; o restante sai na rodada seguinte. Buscar um único membro por ID usa a API REST, e não depende do cache nem da intent de membros.

> **Dica**
>
> Por que 60 segundos? Um cargo que sai com até um minuto de atraso é imperceptível para VIP e castigo. Se o seu caso exige mais precisão, diminua o intervalo, mas lembre que cada remoção é uma chamada à API do Discord e há limites. Veja [como lidar com o rate limit 429](https://streethosting.com.br/guias/bots-discord/corrigir-rate-limit-discord-429).

## Permissão e hierarquia de cargos

Dar e tirar cargo exige a permissão **Manage Roles** (Gerenciar Cargos) no servidor. Só que ter a permissão não basta, porque o Discord aplica uma regra de hierarquia: o bot só mexe em cargos que estão abaixo do seu cargo mais alto na lista de cargos do servidor.

- Em Configurações do servidor, Cargos, arraste o cargo do bot para cima do cargo VIP (ou do cargo de castigo). Se ele estiver abaixo, toda chamada devolve `Missing Permissions`.
- A posição do membro não conta: o bot dá um cargo abaixo do seu até para quem tem cargos mais altos que os dele. A hierarquia do membro só pesa para expulsar, banir e trocar apelido.
- Cargos gerenciados por integração, como o cargo automático de outro bot ou o de impulsionador, não podem ser atribuídos manualmente.
- O cargo temporário não deve ter permissões de administrador. Se o VIP precisa de poucos privilégios, crie um cargo só para isso e dê apenas o necessário.

No código, a propriedade `role.editable` responde se o bot consegue gerenciar aquele cargo, e é o que usamos no comando `/vip` para dar uma mensagem de erro clara em vez de um erro genérico.

> **Atenção**
>
> Dar permissão de administrador ao bot não resolve o erro, porque a hierarquia vale também para quem é administrador. E ainda é arriscado: se o token vazar, o servidor inteiro fica exposto. Dê só Manage Roles e posicione o cargo do bot logo acima dos cargos que ele gerencia.

## Renovar, sair do servidor e voltar

Três situações aparecem na primeira semana de uso e vale tratar todas desde o começo.

- **Renovação.** Com o `ON CONFLICT` acima, um novo `/vip` troca o prazo. Se você prefere somar ao que restava, leia a linha antes e use `Math.max(Date.now(), expiraAtual) + duracaoMs`.
- **Remoção antecipada.** Se um moderador tira o cargo na mão, a linha continua no banco. Não é grave (a remoção do cargo que já não existe é inofensiva), mas dá para limpar escutando `guildMemberUpdate` e apagando a linha quando o cargo some do membro.
- **Quem sai e volta.** O Discord retira os cargos ao sair. Se o prazo ainda não venceu e você quer devolver o benefício, escute `guildMemberAdd`, consulte as linhas daquele usuário e reaplique o cargo. Para castigo isso é essencial, senão sair e entrar vira um jeito de escapar da punição.

Os eventos `guildMemberUpdate` e `guildMemberAdd` só chegam com a intent GuildMembers ligada no cliente e a opção Server Members Intent ativada no portal de desenvolvedores, porque ela é privilegiada. A varredura em si não precisa dela.

Se você vende o VIP, junte essa lógica com o fluxo de pagamento do guia de [loja de vendas no Discord](https://streethosting.com.br/guias/bots-discord/bot-discord-loja-vendas-servidor): o pagamento confirmado chama `darCargoTemporario`. Faça também backup do arquivo do banco, porque ele agora guarda o prazo de quem pagou, como explicado em [backup de dados do bot Discord](https://streethosting.com.br/guias/bots-discord/bot-discord-backup-dados-banco).

## Onde rodar na StreetHosting

A varredura precisa de um processo sempre ligado, e o banco precisa estar em um disco que não se perde entre reinícios. Na [hospedagem de bots da StreetHosting](https://streethosting.com.br/bots) você roda Node.js em um container com painel, e o plano Discord Essential, de R$ 9,99 por mês, inclui 1 GB de RAM, 2 GB de NVMe e um banco MariaDB. Para esse tipo de bot sobra espaço: o SQLite cabe no disco do container e, se preferir, a mesma tabela funciona no MariaDB incluso.

Se o bot é só uma parte de um projeto maior, com site, painel de pagamento e outros serviços, uma [VPS da StreetHosting](https://streethosting.com.br/vps) dá acesso root e você organiza tudo como quiser.

## Perguntas frequentes

### Posso usar só setTimeout para remover o cargo depois de 30 dias?

Não. O temporizador vive na memória do processo e some quando o bot reinicia. Além disso, o setTimeout aceita no máximo cerca de 24,8 dias (2147483647 milissegundos); acima disso o Node dispara quase na hora. Guarde a data de expiração em um banco e confira de tempos em tempos.

### De quanto em quanto tempo a varredura deve rodar?

Uma vez por minuto atende a quase todo servidor. Se o cargo pode sair com alguns minutos de atraso, rodar a cada 5 minutos também serve. O custo é uma consulta indexada no banco, então rodar a cada minuto é barato.

### O bot tem permissão mas o erro Missing Permissions continua. Por quê?

Quase sempre é hierarquia. O cargo mais alto do bot precisa estar acima do cargo que ele vai dar ou tirar, e isso vale mesmo que o bot seja administrador. Cargos de integração (de outros bots ou de boosters) também não podem ser atribuídos por bot.

### O que acontece se o membro sair do servidor antes de o cargo expirar?

O Discord já retira os cargos de quem sai. Na varredura, a busca do membro falha com o código 10007 e você apenas apaga a linha do banco. Se quiser devolver o prazo restante quando ele voltar, use o evento guildMemberAdd e reaplique o cargo.

### Preciso de MariaDB ou o SQLite basta?

Para um bot em poucos servidores, o SQLite com better-sqlite3 basta e não exige serviço extra. Se o bot roda em vários processos ou você já tem um MariaDB, use a mesma tabela lá. A lógica da varredura é idêntica.

## Guias relacionados

- [Como configurar auto role e reaction roles no Discord](https://streethosting.com.br/guias/bots-discord/bot-discord-auto-role-reaction-roles.md)
- [Como agendar mensagens automáticas no Discord com bot](https://streethosting.com.br/guias/bots-discord/bot-discord-agendar-mensagens-discord.md)
- [Comando /lembrete no Discord: bot que avisa por DM depois de 10m, 2h ou 3d](https://streethosting.com.br/guias/bots-discord/bot-discord-lembretes-usuarios.md)
- [Bot de Discord para vendas: como montar uma loja](https://streethosting.com.br/guias/bots-discord/bot-discord-loja-vendas-servidor.md)
- [Como criar bot Discord com slash commands](https://streethosting.com.br/guias/bots-discord/criar-bot-discord-slash-commands.md)

## Produtos citados

- https://streethosting.com.br/bots

## Dados estruturados

```json
[
  {
    "@context": "https://schema.org",
    "@type": [
      "Article",
      "TechArticle"
    ],
    "headline": "Cargos temporários no Discord: VIP de 30 dias que expira sozinho",
    "name": "Cargo temporário no Discord: como o bot remove sozinho depois de X dias",
    "abstract": "Um VIP de 30 dias ou um castigo de 7 dias só funcionam se o cargo sair na hora certa, mesmo que o bot reinicie no meio do caminho. Veja como guardar a data de expiração e remover o cargo com segurança.",
    "description": "Crie cargos temporários no Discord com discord.js v14: salve a data de expiração em um banco, varra a cada minuto e remova o cargo mesmo após reiniciar o bot.",
    "datePublished": "2026-11-13",
    "dateModified": "2026-11-13",
    "author": {
      "@type": "Organization",
      "name": "Equipe StreetHosting"
    },
    "publisher": {
      "@type": "Organization",
      "name": "StreetHosting",
      "url": "https://streethosting.com.br"
    },
    "inLanguage": "pt-BR",
    "mainEntityOfPage": {
      "@type": "WebPage",
      "@id": "https://streethosting.com.br/guias/bots-discord/bot-discord-cargos-temporarios-expiracao"
    }
  },
  {
    "@context": "https://schema.org",
    "@type": "FAQPage",
    "mainEntity": [
      {
        "@type": "Question",
        "name": "Posso usar só setTimeout para remover o cargo depois de 30 dias?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "Não. O temporizador vive na memória do processo e some quando o bot reinicia. Além disso, o setTimeout aceita no máximo cerca de 24,8 dias (2147483647 milissegundos); acima disso o Node dispara quase na hora. Guarde a data de expiração em um banco e confira de tempos em tempos."
        }
      },
      {
        "@type": "Question",
        "name": "De quanto em quanto tempo a varredura deve rodar?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "Uma vez por minuto atende a quase todo servidor. Se o cargo pode sair com alguns minutos de atraso, rodar a cada 5 minutos também serve. O custo é uma consulta indexada no banco, então rodar a cada minuto é barato."
        }
      },
      {
        "@type": "Question",
        "name": "O bot tem permissão mas o erro Missing Permissions continua. Por quê?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "Quase sempre é hierarquia. O cargo mais alto do bot precisa estar acima do cargo que ele vai dar ou tirar, e isso vale mesmo que o bot seja administrador. Cargos de integração (de outros bots ou de boosters) também não podem ser atribuídos por bot."
        }
      },
      {
        "@type": "Question",
        "name": "O que acontece se o membro sair do servidor antes de o cargo expirar?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "O Discord já retira os cargos de quem sai. Na varredura, a busca do membro falha com o código 10007 e você apenas apaga a linha do banco. Se quiser devolver o prazo restante quando ele voltar, use o evento guildMemberAdd e reaplique o cargo."
        }
      },
      {
        "@type": "Question",
        "name": "Preciso de MariaDB ou o SQLite basta?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "Para um bot em poucos servidores, o SQLite com better-sqlite3 basta e não exige serviço extra. Se o bot roda em vários processos ou você já tem um MariaDB, use a mesma tabela lá. A lógica da varredura é idêntica."
        }
      }
    ]
  }
]
```
