Logo
Nuevos usuarios de RPC: 35% de descuento el primer mesVer oferta
OnFinality Learn
Integración y desarrollo13 min de lectura

Ciclo de vida de eth_newFilter: IDs de filtro, getFilterChanges y expiración

Un análisis profundo de la API de filtros con estado de Ethereum: cómo funcionan los IDs de filtro, por qué getFilterChanges es un cursor destructivo y cómo sobrevivir a la expiración del lado del servidor.

TL;DR

eth_newFilter crea un objeto de filtro del lado del servidor en un nodo específico y devuelve un id como cantidad hexadecimal. Ese id es un identificador de estado local del nodo, no una consulta portable. eth_getFilterChanges es una lectura destructiva: devuelve solo los elementos acumulados desde el sondeo anterior y avanza un cursor interno, por lo que llamarlo dos veces pierde el primer lote. Los filtros expiran tras un periodo de inactividad (documentado por clientes como Geth), después del cual getFilterChanges devuelve un error de filtro no encontrado; eth_uninstallFilter libera un filtro explícitamente. Como los ids de filtro no migran entre nodos, los endpoints con balanceo de carga rompen el uso ingenuo de filtros. Este artículo cubre el ciclo de vida completo, un bucle de sondeo en Node.js ejecutable y una tabla de medición que puedes completar con tu propio endpoint.

Qué es un filtro: estado del lado del servidor identificado por un ID hexadecimal

Un filtro no es una consulta que reenvías. Es un objeto creado en un nodo específico mediante eth_newFilter (para logs), eth_newBlockFilter (para hashes de nuevos bloques) o eth_newPendingTransactionFilter (para hashes de transacciones pendientes). El nodo almacena los criterios del filtro y un cursor interno, y luego devuelve un id como cantidad hexadecimal, por ejemplo 0x1 o 0x7f3a. Ese id es lo único que envías en las llamadas posteriores. La especificación JSON-RPC de Ethereum define estos métodos y sus formas de retorno; la especificación JSON-RPC 2.0 define el sobre de solicitud/respuesta y el objeto de error que se devuelve cuando un id es desconocido.

El modelo mental crítico es que el filtro vive en la memoria del nodo, no en tu solicitud. De esto se derivan dos consecuencias inmediatas. Primero, el id no significa nada para cualquier otro nodo. Segundo, el nodo puede descartar el filtro sin avisarte, por lo que el ciclo de vida importa más que la llamada inicial. Si aún estás comparando este modelo de extracción con las suscripciones push, consulta suscripciones eth_subscribe frente a filtros de sondeo.

Los contratos de los métodos de filtro están definidos por la especificación JSON-RPC de Ethereum para los métodos de filtro, y el sobre de error devuelto para un id de filtro desconocido o expirado sigue la especificación JSON-RPC 2.0. El comportamiento de tiempo de espera por inactividad de un filtro del lado del servidor está documentado por clientes como Geth, por lo que un bucle de sondeo que permanece inactivo mucho tiempo debe recrear el filtro en lugar de asumir que persiste.

  • eth_newFilter: crea un filtro de logs a partir de un objeto de filtro (fromBlock, toBlock, address, topics).
  • eth_newBlockFilter: crea un filtro que acumula hashes de nuevos bloques.
  • eth_newPendingTransactionFilter: crea un filtro que acumula hashes de transacciones pendientes.
  • Los tres devuelven un id como cantidad hexadecimal; el id es estado local del nodo.

Los tres tipos de filtro y qué devuelve getFilterChanges

El tipo de filtro determina el tipo de elemento que devuelve eth_getFilterChanges. Un filtro de logs devuelve un array de objetos de log, cada uno con address, topics, data, blockNumber, transactionHash y campos relacionados. Un filtro de bloques devuelve un array de hashes de bloque como cadenas hexadecimales. Un filtro de transacciones pendientes devuelve un array de hashes de transacción. El array está vacío cuando no ha llegado nada nuevo desde el último sondeo.

Esta diferencia de tipos es una fuente común de errores de integración: el código escrito para un filtro de logs que asume objetos se romperá cuando se apunte a un filtro de bloques que devuelve cadenas. Si tu objetivo es leer logs completos de un rango conocido en lugar de rastrear nuevos, la ruta sin estado de filtrado de temas de eventos con eth_getLogs suele ser más sencilla y no tiene ningún id que gestionar.

  • Filtro de logs -> array de objetos de log.
  • Filtro de bloques -> array de hashes de bloque.
  • Filtro de transacciones pendientes -> array de hashes de transacción.
  • Un array vacío significa que no hay elementos nuevos desde el sondeo anterior, no un error.

Semántica del cursor: getFilterChanges frente a getFilterLogs

eth_getFilterChanges es una lectura destructiva. Devuelve solo los elementos acumulados desde la llamada anterior y avanza el cursor interno del filtro más allá de ellos. Llamarlo dos veces seguidas devuelve el primer lote y luego un array vacío (o solo los elementos recién llegados). Por eso el sondeo repetido es el patrón normal y por lo que llamarlo accidentalmente desde dos rutas de código pierde datos: el segundo llamador consume lo que el primero debería haber visto.

eth_getFilterLogs es la contraparte no destructiva. Devuelve el conjunto completo de logs que coinciden con los criterios del filtro, no solo el delta. Es útil como comprobación de reconciliación: tras un lote perdido sospechoso, llama a getFilterLogs para ver el conjunto completo de coincidencias y compáralo con lo que registró tu bucle basado en cursor. Los dos métodos responden a preguntas diferentes, y mezclarlos sin entender el cursor es una causa documentada de logs duplicados o perdidos. Para una discusión relacionada sobre reintentos seguros, consulta idempotencia JSON-RPC y seguridad frente a solicitudes duplicadas.

  • getFilterChanges: delta desde el último sondeo, avanza el cursor, destructivo.
  • getFilterLogs: conjunto completo de logs coincidentes, no avanza el cursor.
  • Sondear getFilterChanges en bucle es lo esperado; llamarlo dos veces descarta el primer resultado.
  • Usa getFilterLogs para reconciliar, no como reemplazo directo en el bucle de sondeo.

Ciclo de vida y expiración: los filtros inactivos se descartan

Un filtro es estado del lado del servidor con un tiempo de espera por inactividad. La documentación de Geth describe que los filtros se eliminan tras un periodo sin sondeo, y otros clientes documentan un comportamiento similar. Una vez que se descarta un filtro, eth_getFilterChanges para ese id devuelve un error de filtro no encontrado en lugar de un array vacío. La ventana de tiempo de espera exacta depende del cliente y la versión, así que trátala como un comportamiento documentado que varía según el proveedor, no como una constante fija.

eth_uninstallFilter libera un filtro explícitamente y devuelve un booleano que indica si el filtro existía. Un consumidor que permanece inactivo mucho tiempo debe, por tanto, gestionar el descarte: capturar el error de filtro no encontrado, recrear el filtro y reanudar el sondeo. Recrear desde el último bloque procesado evita un hueco. El eth_getLogs sin estado no tiene ese tiempo de espera porque no mantiene estado del lado del servidor, lo que es una razón clave por la que algunos equipos lo prefieren para consumidores de baja frecuencia.

  • Los filtros inactivos se descartan tras un periodo de inactividad definido por el cliente.
  • Tras la expiración, getFilterChanges devuelve un error de filtro no encontrado, no un array vacío.
  • eth_uninstallFilter libera un filtro explícitamente y devuelve un booleano.
  • Recrea ante filtro no encontrado y reanuda desde el último bloque procesado.

Por qué los IDs de filtro no migran entre nodos

Como el filtro es estado local del nodo, un id creado contra un endpoint es desconocido para otro. Si tu cliente envía eth_newFilter al nodo A y luego eth_getFilterChanges al nodo B a través de un balanceador de carga round-robin, el nodo B nunca ha visto ese id y devuelve un error de filtro no encontrado. Este es uno de los fallos de producción más comunes con la API de filtros y es invisible en pruebas con un solo nodo.

La solución es fijar el tráfico de filtros al nodo que creó el filtro, o evitar por completo la API con estado en favor del sondeo sin estado con eth_getLogs. Si debes usar balanceo de carga, usa sesiones persistentes o una única conexión dedicada durante la vida del filtro. El comportamiento del proveedor aquí varía: algunos endpoints gestionados documentan enrutamiento persistente, otros no, así que verifícalo con tu propio endpoint. Para orientación sobre selección de endpoints, consulta Endpoints RPC de Ethereum y selección de proveedor (RPC Assistant).

  • Los ids de filtro son estado por nodo y no son portables.
  • El balanceo de carga round-robin rompe el uso ingenuo de filtros.
  • Fija el tráfico de filtros al nodo creador o usa eth_getLogs sin estado.
  • La compatibilidad con enrutamiento persistente varía según el proveedor; verifícala empíricamente.

Ejemplo ejecutable en Node.js: crear, sondear, reconciliar y desinstalar

El ejemplo siguiente usa el fetch integrado (Node.js 18+) y ninguna dependencia externa. Crea un filtro de logs, sondea eth_getFilterChanges a intervalos, usa eth_getFilterLogs como comprobación de reconciliación, desinstala el filtro al apagar y lo recrea cuando se produce un error de filtro no encontrado. Sustituye la URL del endpoint y los criterios del filtro por tus propios valores.

La rama de recreación ante error es la parte que más integraciones omiten. Sin ella, un periodo de silencio superior al tiempo de espera por inactividad del nodo termina tu flujo en silencio. La llamada de reconciliación es opcional pero útil durante el desarrollo para confirmar que el cursor se comporta como se espera.

const ENDPOINT = process.env.ETH_RPC_URL || 'https://your-endpoint.example';
const FILTER = { fromBlock: 'latest', address: null, topics: [] };

let filterId = null;
let running = true;

async function rpc(method, params) {
  const res = await fetch(ENDPOINT, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ jsonrpc: '2.0', id: Date.now(), method, params })
  });
  const json = await res.json();
  if (json.error) {
    const err = new Error(json.error.message);
    err.code = json.error.code;
    throw err;
  }
  return json.result;
}

async function createFilter() {
  filterId = await rpc('eth_newFilter', [FILTER]);
  console.log('created filter', filterId);
}

async function poll() {
  try {
    const changes = await rpc('eth_getFilterChanges', [filterId]);
    if (changes.length) console.log('new logs', changes.length);
  } catch (err) {
    if (err.code === -32000 || /filter not found/i.test(err.message)) {
      console.warn('filter expired, recreating');
      await createFilter();
    } else {
      throw err;
    }
  }
}

async function reconcile() {
  const all = await rpc('eth_getFilterLogs', [filterId]);
  console.log('full matching set', all.length);
}

async function shutdown() {
  running = false;
  if (filterId) {
    const ok = await rpc('eth_uninstallFilter', [filterId]);
    console.log('uninstalled', filterId, ok);
  }
}

(async () => {
  await createFilter();
  process.on('SIGINT', async () => { await shutdown(); process.exit(0); });
  while (running) {
    await poll();
    await new Promise(r => setTimeout(r, 5000));
  }
})();

Tabla de resultados: mide el comportamiento de los filtros en tu propio endpoint

El comportamiento de los filtros depende del cliente y del proveedor, así que la única respuesta fiable es la que mides tú. Ejecuta la sonda de abajo contra tu endpoint, deja un filtro inactivo durante un periodo conocido y luego sondea y registra qué ocurre. Completa la tabla con tus observaciones. No asumas que los valores de la documentación de otro proveedor se aplican al tuyo.

La sonda crea un filtro de bloques, registra el formato del id, espera y luego intenta un sondeo. Ajusta el periodo de inactividad para acotar el tiempo de espera sospechado. Un objeto de error JSON-RPC con un código y un mensaje indica que el filtro fue descartado; un array vacío indica que sigue vivo.

// probe.js - run with: node probe.js
const ENDPOINT = process.env.ETH_RPC_URL || 'https://your-endpoint.example';

async function rpc(method, params) {
  const res = await fetch(ENDPOINT, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
  });
  return res.json();
}

(async () => {
  const created = await rpc('eth_newBlockFilter', []);
  console.log('id format:', created.result);
  const idleMs = Number(process.env.IDLE_MS || 300000);
  console.log('idling for', idleMs, 'ms');
  await new Promise(r => setTimeout(r, idleMs));
  const polled = await rpc('eth_getFilterChanges', [created.result]);
  console.log('after idle:', JSON.stringify(polled));
  const unknown = await rpc('eth_getFilterChanges', ['0xdeadbeef']);
  console.log('unknown id:', JSON.stringify(unknown));
})();

Modos de fallo y solución de problemas

La mayoría de los incidentes con filtros caen en cuatro categorías. La primera es filtro no encontrado tras un periodo de inactividad: el consumidor estuvo inactivo más tiempo que el tiempo de espera por inactividad del nodo y el siguiente sondeo dio error. La solución es capturar el error y recrear el filtro desde el último bloque procesado. La segunda es un filtro creado en un endpoint con balanceo de carga que hace round-robin hacia un nodo que nunca lo vio; la solución es enrutamiento persistente o sondeo sin estado.

La tercera son logs duplicados o perdidos por mezclar getFilterChanges y getFilterLogs. Como getFilterChanges avanza el cursor y getFilterLogs no, usar ambos en el mismo bucle sin rastrear qué elementos ya has procesado lleva a doble conteo o huecos. La cuarta es un estado de filtros que crece sin control: si los filtros nunca se desinstalan, el nodo los acumula hasta que expiran o el proceso se reinicia. Llama siempre a eth_uninstallFilter al apagar. Para huecos relacionados con reorganizaciones, consulta Detección de reorganizaciones de bloques por RPC.

  • Filtro no encontrado tras inactividad: recrea el filtro y reanuda desde el último bloque procesado.
  • Endpoint con balanceo de carga: fija el tráfico de filtros o cambia a eth_getLogs sin estado.
  • Métodos de cursor mezclados: rastrea explícitamente los elementos procesados para evitar duplicados o huecos.
  • Nunca desinstalados: los filtros se acumulan hasta la expiración o el reinicio; desinstálalos al apagar.

Orientación operativa: cadencia de sondeo, idempotencia y apagado

Una vez entendido el modelo de filtros, los detalles operativos deciden si un sondeador es fiable. Sondea con una cadencia más rápida que el tiempo de espera por inactividad del nodo para que el filtro nunca se descarte, pero no tan rápida que cada llamada vacía a getFilterChanges cueste un viaje de ida y vuelta para nada; una cadencia de unos pocos segundos es el compromiso habitual, ajustada al tiempo de bloque de la cadena. Como getFilterChanges es una lectura destructiva, el consumidor debe persistir lo que haga con los elementos devueltos antes del siguiente sondeo, o los elementos se pierden en el momento en que el cursor avanza.

Desinstalar los filtros al apagar mantiene acotado el estado de filtros del nodo. Un proceso que se bloquea sin llamar a eth_uninstallFilter deja que el filtro expire por sí solo, lo cual es aceptable, pero un proceso que crea un filtro nuevo en cada reinicio sin desinstalar acumula estado. Trata el id de filtro como un recurso con una liberación explícita, exactamente como harías con un cursor de base de datos, y recréalo —nunca lo reanudes— tras cualquier reinicio o error de filtro no encontrado.

  • Sondea más rápido que el tiempo de espera por inactividad, pero no más rápido de lo que la cadena produce nuevos logs.
  • Persiste o reenvía cada elemento antes de la siguiente llamada a getFilterChanges; la lectura es destructiva.
  • Llama a eth_uninstallFilter al apagar y recrea el filtro tras cualquier expiración o reinicio.
  • Nunca guardes en caché un id de filtro entre cambios de endpoint: es estado local del nodo.

Limitaciones y compensaciones: cuándo la API de filtros es la herramienta equivocada

La API de filtros con estado es cómoda para consumidores de alta frecuencia que sondean lo suficiente para mantenerse dentro de la ventana de inactividad, pero conlleva costes reales. Requiere una conexión persistente a un nodo, no tiene un id portable y puede descartarse en silencio. Algunos proveedores han dejado obsoletos los métodos de filtro en favor del sondeo sin estado con eth_getLogs o del push con eth_subscribe, y las superficies disponibles varían según el proveedor. Confirma siempre con la documentación de tu endpoint antes de diseñar en torno a filtros.

Para consumidores de baja frecuencia, reconciliación por lotes o cualquier carga de trabajo que deba sobrevivir a reinicios de nodos, eth_getLogs sin estado suele ser la mejor opción: sin id, sin cursor, sin expiración. Para consumidores de alta frecuencia y baja latencia que pueden mantener una conexión, eth_subscribe suele ser preferible. La API de filtros se sitúa entre ambos y es mejor tratarla como una herramienta especializada que como una opción predeterminada. Si lees recibos en bloque, eth_getBlockReceipts frente a lecturas individuales de recibos cubre un patrón sin estado relacionado.

  • Con estado: requiere una conexión persistente a un nodo.
  • No portable: los ids no migran entre nodos.
  • Puede descartarse en silencio tras un periodo de inactividad.
  • La disponibilidad varía según el proveedor; algunos dejan obsoletos los filtros en favor de eth_getLogs o eth_subscribe.

Próximos pasos: elegir entre filtros, getLogs y suscripciones

Decide según la frecuencia de sondeo y la estabilidad de la conexión. Si sondeas cada pocos segundos y puedes mantener una conexión a un nodo, la API de filtros es viable siempre que gestiones la expiración y la desinstalación. Si sondeas con poca frecuencia o necesitas sobrevivir a reinicios, usa eth_getLogs sin estado. Si necesitas semántica push y tu proveedor la admite, usa eth_subscribe. El centro de aprendizaje de OnFinality tiene artículos complementarios sobre cada patrón.

Antes de comprometerte, ejecuta la sonda de arriba contra tu endpoint y completa la tabla de resultados. Esa medición, y no la documentación de otro proveedor, es la base de tu diseño. Para opciones de endpoint y selección de proveedor, consulta Endpoints RPC de Ethereum y selección de proveedor (RPC Assistant), y para detalles específicos de la red, consulta Ethereum en OnFinality. Si estás planificando capacidad, las páginas de precios de RPC y del servicio de API describen las superficies comerciales.

  • Alta frecuencia + conexión estable: API de filtros con gestión de expiración.
  • Baja frecuencia o tolerante a reinicios: eth_getLogs sin estado.
  • Semántica push disponible: eth_subscribe.
  • Mide siempre tu propio endpoint antes de diseñar en torno a filtros.

Nunca te preocupes por la infraestructura nuevamente

OnFinality elimina la carga pesada de DevOps para que puedas construir de forma más inteligente y rápida.

Comenzar