
Trimtok nació de una necesidad concreta: quería una herramienta sencilla para descargar videos de TikTok, recortarlos por rango de tiempo y exportarlos como mp4 o MP3, sin depender de servicios de terceros. Lo que empezó como un script local terminó convirtiéndose en una arquitectura serverless completa desplegada en AWS.
En este post hago un recorrido detallado por la arquitectura de Trimtok, explicando las decisiones de diseño, los servicios de AWS utilizados, trade-offs y consideraciones, y los costos estimados.
La arquitectura de Trimtok es completamente serverless y orientada a eventos. No hay servidores que gestionar, no hay contenedores que escalar manualmente. Cada componente se activa exactamente cuando se necesita y se apaga cuando termina su trabajo.
A continuación describo los principales servicios y su función en la arquitectura, con los fragmentos del IaC en TypeScript gestionado con SST 4 (Ion/Pulumi).
DynamoDB es la base de datos principal de Trimtok. En lugar de múltiples tablas, utilizo un único esquema (TrimtokTable) que aloja todas las entidades del dominio: Jobs, artefactos en caché, conexiones WebSocket activas y locks distribuidos para deduplicación.
El diseño single-table permite acceder a cualquier entidad en O(1) con la clave primaria pk + sk, y el GSI1 resuelve el único patrón de acceso que no cabe en la clave principal: encontrar todas las conexiones WebSocket suscritas a un job específico para enviar la notificación de completado.
El campo expiresAt activa el TTL nativo de DynamoDB para limpiar registros automáticamente (7 días por defecto), sin necesidad de lambdas de limpieza.
Las entidades modeladas en la tabla son:
| Entidad | PK | SK | Propósito |
|---|---|---|---|
| Job | JOB#{jobId} | METADATA | Estado y metadatos del job |
| CacheArtifact | ARTIFACT#{videoId} | {FORMAT}#{type}#{trimStart}#{trimEnd} | Índice S3 para cache hits |
| DistributedLock | LOCK#{videoId} | LOCK | Mutex para descargas concurrentes |
| WebSocketConnection | CONN#{connId} | METADATA | Conexiones WS activas |
| ProcessingEvent | JOB#{jobId} | EVENT#{timestamp}#{ulid} | Auditoría inmutable |
S3 almacena todos los artefactos generados: videos originales descargados, recortes MP4, GIFs y MP3. El acceso a los artefactos siempre se hace a través de presigned URLs con validez de 1 hora — Lambda nunca actúa como proxy de bytes, lo que evita costos de transferencia innecesarios y mantiene el payload de las respuestas pequeño.
Cada tipo de artefacto tiene una política de ciclo de vida diferente según su criticidad:
originals/: 48 horas (el video base que permite regenerar cualquier derivado)trims/, gifs/, mp3s/trims/: 24 horas (derivados regenerables fácilmente)mp3s/originals/: 48 horas (mismo ciclo que el video original)Cada tipo de operación tiene su propia cola SQS con su Dead Letter Queue dedicada. Esta separación es deliberada: una descarga puede tardar varios minutos (yt-dlp + upload a S3 de 50–150 MB), mientras que un recorte con -c copy suele resolverse en segundos.
Todas las colas comparten la misma configuración de timeout y permiten hasta 2 reintentos antes de enviar el mensaje a la DLQ.
CloudWatch Alarms monitorean las DLQs: cualquier mensaje que llegue a ellas genera una alarma que me permite detectar fallos en los workers de forma proactiva.
yt-dlp y ffmpeg son binarios externos que no pueden instalarse como dependencias npm. La solución son Lambda Layers: archivos ZIP con los binarios precompilados para ARM64 (Amazon Linux 2023) que se montan en /opt/bin/ dentro de cada función Lambda.
El caso de ffmpeg merece mención especial: el binario pesa aproximadamente 150 MB, superando el límite de 70 MB para uploads directos de Lambda. La solución es subirlo primero a S3 y referenciar ese objeto en la definición del Layer. En este caso se subieron ambos binarios a S3, aunque yt-dlp podría haberse incluido directamente en el paquete de la función al pesar menos de 70 MB.
El procesamiento de videos es asíncrono: el usuario no puede estar en un loading spinner bloqueante durante varios minutos. La solución natural serían los Server-Sent Events (SSE), pero API Gateway tiene un límite de 29 segundos por request que los hace inviables.
API Gateway WebSocket resuelve esto limpiamente: el frontend abre una conexión persistente, envía un mensaje { action: "subscribe", jobId }, y cuando el worker termina su trabajo, hace un POST a la Management API de API GW para empujar la notificación directamente a la conexión del cliente.
La gestión de las conexiones activas vive en DynamoDB: al conectar se escribe CONN#{connId}, al suscribirse se actualiza con el jobId y las claves del GSI1 (gsi1pk = JOB#{jobId}, gsi1sk = CONN#{connId}). Esto permite al worker hacer una query en GSI1 para encontrar todas las conexiones suscritas a un job y notificarlas todas de una vez.
La API HTTP expone cinco endpoints síncronos. La configuración de CORS es estricta en producción: solo acepta peticiones desde trimtok.pool-llerena.com. El endpoint directo de API Gateway está deshabilitado (disableExecuteApiEndpoint: true) en producción para forzar todo el tráfico a pasar por Cloudflare, que actúa como proxy.
La autenticación se implementa con un Lambda Authorizer que verifica un header secreto Custom-secret inyectado por Cloudflare en cada request. Esto protege la API de llamadas directas sin necesidad de implementar JWT o sesiones de usuario en esta primera versión.
Cuatro funciones Lambda (ARM64, 1 GB de RAM) procesan los mensajes de sus respectivas colas. Cada worker sigue el mismo patrón: leer el mensaje, ejecutar el binario en /tmp, subir el resultado a S3, actualizar el estado del Job en DynamoDB y notificar al cliente por WebSocket.
| Worker | Binario | Operación |
|---|---|---|
download-worker | yt-dlp + ffmpeg | Descarga el video original y lo sube a originals/ |
trim-worker | ffmpeg | -ss {start} -to {end} -c copy (stream copy, sin re-encode) |
gif-worker | ffmpeg | Dos pasadas: palettegen + paletteuse para GIF de alta calidad |
mp3-worker | ffmpeg | -vn -acodec libmp3lame -q:a 2 para audio en alta calidad |
La opción dev: false es importante: evita que SST intente sustituir estas funciones por un proxy de desarrollo local durante sst dev, ya que no tiene sentido ejecutar yt-dlp o ffmpeg en una máquina de desarrollo de esta forma. partialResponses: true permite que un batch con múltiples mensajes reporte los fallos individualmente en lugar de rechazar todo el batch.
El download-worker además implementa deduplicación con un mutex distribuido: ejecuta un PutItem condicional (attribute_not_exists(pk)) para crear el lock LOCK#{videoId}. Si otro worker ya está descargando el mismo video, la escritura falla y el segundo worker puede esperar a que el artefacto aparezca en el caché antes de responder.
El flujo completo desde que el usuario pega una URL hasta que descarga el archivo:
API Gateway tiene un límite de 29 segundos por request, lo que elimina directamente SSE y long-polling como opciones viables. La alternativa habría sido polling corto (cada 2–3 segundos), pero introduce latencia perceptible y carga innecesaria. API Gateway WebSocket resuelve el problema de raíz, aunque añade algo de complejidad: hay que gestionar conexiones, suscripciones y desconexiones.
Un diseño multi-tabla hubiera sido más sencillo de razonar inicialmente, pero habría implicado múltiples tablas de DynamoDB (jobs, artifacts, connections, locks), cada una con su propio punto de configuración, monitoreo y facturación. El single-table design centraliza todo y funciona bien porque los patrones de acceso están bien definidos desde el diseño: la clave primaria resuelve el 90% de los casos, y GSI1 cubre el patrón restante (encontrar conexiones por jobId).
Una cola única hubiera mezclado operaciones de tiempos radicalmente distintos. Si el worker de descarga falla repetidamente y llena la DLQ, habría bloqueado también los recortes y los GIFs. Colas separadas permiten:
La alternativa habría sido incluir los binarios directamente en el paquete de la función. El problema es que el límite de despliegue de Lambda es 250 MB sin comprimir. yt-dlp y ffmpeg juntos superan ese límite. Los Lambda Layers además permiten actualizar los binarios de forma independiente al código de la función, lo que es útil cuando yt-dlp lanza una nueva versión para soportar cambios en la API de TikTok.
48 horas para los originals y 24 horas para los derivados son ventanas suficientes para la mayoría de los casos de uso (el usuario descarga el archivo inmediatamente o en las siguientes horas). TTLs más largos aumentarían el costo de almacenamiento sin proporcionar valor real, dado que el usuario puede regenerar cualquier derivado siempre que el original siga en S3.
SST (Ion/Pulumi) permite describir la infraestructura en TypeScript con el mismo lenguaje que el resto del código del proyecto. La integración entre recursos es directa (.link() comparte automáticamente variables de entorno y permisos IAM mínimos entre funciones). La alternativa hubiera sido CDK o Terraform, pero SST ofrece una experiencia de desarrollo local mucho más ergonómica con sst dev.
Los costos mensuales estimados son bajos gracias a la naturaleza serverless y al uso del AWS Free Tier.
| Servicio | Bajo (1K req/mes) | Moderado (10K req/mes) | Alto (100K req/mes) |
|---|---|---|---|
| Lambda | ~$0.00 | ~$0.02 | ~$32.00 |
| DynamoDB (on-demand) | ~$0.01 | ~$0.07 | ~$0.69 |
| S3 (almacenamiento + req) | ~$0.38 | ~$3.89 | ~$38.85 |
| SQS | ~$0.00 | ~$0.00 | ~$0.00 |
| API GW HTTP v2 | ~$0.00 | ~$0.04 | ~$0.40 |
| API GW WebSocket | ~$0.01 | ~$0.06 | ~$0.61 |
| Total mensual | ~$0.40 | ~$4.08 | ~$72.55 |
Construir Trimtok fue un reto en el que cada decisión de arquitectura tuvo que justificarse con las restricciones reales de la plataforma: el límite de 29 segundos de API Gateway forzó el uso de WebSocket, el tamaño de ffmpeg forzó el upload vía S3, y los tiempos de ejecución heterogéneos de las operaciones justificaron las cuatro colas separadas.
El resultado es una arquitectura que escala de cero a miles de requests por mes sin cambiar una sola línea de IaC, con un costo marginal en tráfico bajo y sin servidores que mantener. SST 4 hizo que toda la infraestructura fuese describible en el mismo TypeScript que el resto del proyecto, lo que reduce significativamente la fricción entre el código de la aplicación y la infraestructura que lo soporta.
Los próximos pasos naturales son implementar autenticación real para persistir el historial de descargas del usuario, explorar el uso de S3 Intelligent-Tiering para reducir costos de almacenamiento en escenarios de alto tráfico y agregar cloudfront delante de S3 para mejorar la latencia, disponibilidad, seguridad y reducir costos de transferencia a internet. Pero incluso en su estado actual, Trimtok es una herramienta robusta, escalable y eficiente que cumple perfectamente con su propósito.