aws.crafter.run

Comunicación

Claim-check

También llamado Claim-check

Guardar el contenido pesado en S3 y hacer viajar por el sistema de mensajería solo una referencia, para que los límites de tamaño de los mensajes dejen de decidir tu arquitectura.

DirectoVerificado el

El problema

Tu sistema procesa facturas. Cuando llega una, publicas un evento con los datos para que el resto reaccione. Un día alguien adjunta el PDF al evento y todo se rompe.

El motivo es que cada tramo de un sistema serverless tiene su propio techo de tamaño, y son distintos:

Tramo Techo
Publicación en SNS 256 KiB
Mensaje en SQS 1 MiB
Payload síncrono de Lambda 6 MB
Payload asíncrono de Lambda 1 MB
Payload de API Gateway 10 MB
Entrada o salida de un estado de Step Functions 256 KiB
Objeto en S3 48 TB

El que manda es el más bajo de la cadena, y casi nunca es el que estás mirando. Un evento de 400 KB cabe de sobra en tus colas y aun así el Publish de SNS lo rechaza. Un documento de 2 MB pasa por API Gateway y muere en la Lambda. Un JSON que crece estado a estado revienta a mitad de una saga, con la mitad de los efectos ya aplicados.

La solución

Separar el contenido de la notificación. El productor guarda el payload en S3 y publica un mensaje minúsculo con la referencia: bucket, clave, y los metadatos que los consumidores necesitan para decidir si les interesa.

productorS3payload · 3 MBSNSreferencia · 200 Bconsumidor1 · guarda el payload2 · publica la clavereferencia3 · lee el payload solo si lo necesitaEl techo de 256 KiB de SNS deja de aplicar · el ciclo de vida de S3 se encarga de la limpieza
Por el sistema de mensajería viaja el resguardo, no la maleta. El límite de 256 KiB de SNS deja de importar porque el mensaje pesa unos cientos de bytes, y el consumidor decide si necesita descargar el contenido o le basta con los metadatos.

El nombre viene de la ficha del guardarropa: dejas el abrigo, te llevas un número. Por el sistema circula el resguardo, y el abrigo se queda quieto donde lo dejaste.

Tres ventajas, y la tercera es la que no se ve al principio:

Cuándo usarlo

Cuándo NO usarlo

Cómo implementarlo

  1. Escribe primero en S3, publica después. Al revés garantiza mensajes que apuntan a nada.
  2. Genera la clave con un identificador del hecho, no con un nombre de archivo del usuario.
  3. Mete en el mensaje los metadatos que permiten filtrar —tipo, tamaño, propietario— para que un consumidor pueda descartar sin descargar. Es lo que hace que las filter policies sigan sirviendo.
  4. Versiona el formato del mensaje: un campo v que te deje cambiar la forma de la referencia más adelante.
  5. Pon una regla de ciclo de vida en el prefijo del claim-check. El payload sobrevive al mensaje si nadie lo borra.
  6. Da permiso de lectura a los consumidores sobre ese prefijo, no sobre el bucket entero.

El código

// ── Productor ──────────────────────────────────────────────────────
const clave = `claims/${new Date().toISOString().slice(0, 10)}/${ulid()}`;

// Primero el contenido. Si esto falla, no hay mensaje huerfano.
await s3.send(new PutObjectCommand({
  Bucket: BUCKET,
  Key: clave,
  Body: pdf,
  ContentType: 'application/pdf',
}));

await sns.send(new PublishCommand({
  TopicArn: TOPIC,
  // El mensaje lleva la referencia y lo justo para decidir sin descargar.
  Message: JSON.stringify({
    v: 1,
    tipo: 'FacturaRecibida',
    facturaId: factura.id,
    proveedor: factura.proveedor,
    importe: factura.importe,
    contenido: { bucket: BUCKET, clave, bytes: pdf.length },
  }),
  MessageAttributes: {
    tipo: { DataType: 'String', StringValue: 'FacturaRecibida' },
  },
}));
// ── Consumidor que NO necesita el PDF ──────────────────────────────
export const registrarFactura = async (evento: SQSEvent) => {
  for (const r of evento.Records) {
    const m = JSON.parse(r.body);
    // Le basta con los metadatos: no toca S3 en ningun momento.
    await guardar({ id: m.facturaId, proveedor: m.proveedor, importe: m.importe });
  }
};

// ── Consumidor que SI lo necesita ──────────────────────────────────
export const extraerTexto = async (evento: SQSEvent) => {
  for (const r of evento.Records) {
    const m = JSON.parse(r.body);
    const obj = await s3.send(new GetObjectCommand({
      Bucket: m.contenido.bucket,
      Key: m.contenido.clave,
    }));
    await indexar(m.facturaId, await extraer(obj.Body));
  }
};

Te va a morder

Coste

Concepto Con payload en el mensaje Con claim-check
Petición de SNS (256 KB) 4 peticiones 1
Petición de SQS (256 KB) 4 peticiones 1
Escritura en S3 1 PUT
Lectura en S3 1 GET por consumidor que lo necesite
Almacenamiento mientras dure el ciclo de vida

La regla de los 64 KB es la que decide: SNS y SQS facturan cada porción de 64 KB como una petición, así que un mensaje de 256 KB cuesta cuatro. Con la referencia cuesta una, y a cambio pagas un PUT y los GET de quien de verdad descargue.

El patrón gana claramente cuando hay más consumidores que descargas: cinco consumidores de los que solo uno abre el PDF pagan un transporte barato cinco veces y una lectura cara una vez. Pierde si todos descargan siempre y los mensajes eran pequeños — pero entonces tampoco tenías un problema de tamaño.

Fuentes

Tamaño máximo de mensaje de SNS y regla de facturación por porciones de 64 KB: Amazon SNS endpoints and quotas. Tamaño máximo de mensaje de SQS y Extended Client Libraries hasta 2 GB: Amazon SQS message quotas. Payloads síncrono y asíncrono de Lambda: Lambda quotas. Límite de 256 KiB entre estados de Step Functions: Step Functions service quotas. Tamaño máximo de objeto en S3: Amazon S3 endpoints and quotas.

Patrones relacionados

Un enlace sin la relación nombrada es un "ver también". Aquí cada uno dice qué relación tiene y por qué.