Cloudflare R2 Direct Upload: Cero Costos de Servidor

La tragedia de la carga por retransmisión en el backend: Tiempos de espera de memoria y cargos por ancho de banda
El error arquitectónico más común que cometen los desarrolladores en aplicaciones web y móviles al procesar imágenes de gran tamaño, videos de alta resolución y documentos PDF pesados es el método de carga por retransmisión (Relay), transmitido a través de la ruta “Cliente -> Servidor API Backend -> Almacenamiento de Objetos (S3/R2)”.
[Método ineficiente de carga por retransmisión en el backend]
[Client] --- (Carga de archivo de 100MB) ---> [Node.js / Lambda Server] --- (Reenvío de 100MB) ---> [Storage]
* Desbordamiento de búfer en memoria RAM del servidor
* Renovación del tiempo de ejecución de CPU y timeouts
* Costos duplicados de ancho de banda y Egress
Este método de retransmisión provoca los siguientes cuellos de botella críticos en la nube:
- Consumo excesivo de tiempo de cómputo y memoria del servidor: Al almacenar en búfer archivos de gran tamaño entre 100MB y 1GB en la memoria del servidor backend, se producen errores de falta de memoria (OOM Error) o la comunicación HTTP se mantiene durante más de 30 segundos, provocando la terminación por tiempo de espera (timeout) en AWS Lambda o Cloudflare Workers.
- Costos duplicados de ancho de banda: Al recibir datos voluminosos del cliente al servidor y luego reenviarlos desde el servidor al almacenamiento S3, las tarifas de entrada (inbound) y salida (outbound) de red se duplican.
- Cuello de botella de infraestructura: El escalado de la infraestructura del servidor queda limitado por el ancho de banda de carga de archivos, lo que degrada la velocidad general del servicio.
La arquitectura de URLs pre-firmadas (Presigned URLs) de Cloudflare R2 y Carga Directa (Direct Upload) resuelve este problema a la perfección.
El servidor backend (Cloudflare Worker) solo tarda 1ms en emitir una URL PUT pre-firmada con cifrado de seguridad, y el cliente (Web / Flutter) transmite el archivo directamente (1:1) al bucket de almacenamiento Cloudflare R2 a través de esta URL.
Este enfoque no solo reduce el consumo de cómputo del servidor backend en un 99%, sino que también se combina con el beneficio exclusivo de Cloudflare R2 de “tarifa de salida Egress de $0” para ofrecer un canal de carga de archivos de gran tamaño con costo de $0.
En este artículo, abordamos en detalle desde los principios de funcionamiento de las URLs pre-firmadas hasta la carga secuencial de un solo archivo, la carga multipart (Multipart Upload) para archivos de 100MB a 5GB, la configuración de seguridad CORS y pruebas de rendimiento (benchmarks) reales.
Comparación de arquitecturas de carga directa en R2
| Criterio de comparación | Carga por retransmisión tradicional en backend | Carga directa con Presigned URLs en Cloudflare R2 |
|---|---|---|
| Ruta de movimiento de datos | Client -> Server -> Storage | Client -> R2 Storage (Direct 1:1) |
| Uso de CPU/Memoria del servidor | Alto (Almacenamiento en búfer RAM según tamaño de archivo) | ZERO (Liberación inmediata tras generar firma en 1ms) |
| Riesgo de timeout en servidor | Muy alto (Fallo en transferencia de archivos grandes) | Ninguno (El Worker finaliza tras emitir la URL) |
| Soporte multipart para archivos grandes | Implementación compleja y carga extrema en servidor | Carga paralela por partes de archivos de 5GB mediante API en el edge |
| Costo de red Egress | Aplica (Cobro por salida por GB en AWS S3) | $0 (Sin costo de Egress en Cloudflare R2) |
| Seguridad de carga (Security) | Posible exposición de API Keys en el servidor | Pre-firma HMAC-SHA256 con límite TTL de 5 minutos |
Principio de funcionamiento de R2 Direct Upload: Cifrado de URLs PUT pre-firmadas
En Cloudflare Workers, es posible utilizar las librerías de cifrado estándar de la API de AWS S3 (@aws-sdk/client-s3) y @aws-sdk/s3-request-presigner.
+-----------------------------------------------------------------------------------+
| Canalización de Cloudflare R2 Presigned Direct Upload |
+-----------------------------------------------------------------------------------+
[Client (Web / Flutter)] [Cloudflare Worker (1ms)] [Cloudflare R2 Bucket]
| | |
|--- 1. Solicitud de carga (nombre/tamaño) ->| |
| |--- 2. Generar URL pre-firmada HMAC ->| (Presigned PUT URL)
|<-- 3. Devolver URL pre-firmada -| |
| |
|----------------------- 4. Direct HTTP PUT (Transferencia binaria) ->|
|<---------------------- 5. 200 OK (Carga completada) ---------------|
- Solicitud de firma: El cliente envía el nombre del archivo (
video.mp4), el mimeType (video/mp4) y el tamaño al Worker. - Generación de URL PUT pre-firmada: El Worker utiliza el algoritmo HMAC-SHA256 de AWS S3 para emitir una URL firmada con cifrado válida por solo 5 minutos en 1ms.
- Envío Direct HTTP PUT: El cliente realiza una solicitud HTTP PUT directamente con la URL pre-firmada obtenida para transferir los datos a R2 de forma automatizada.
Paso 1: Creación de bucket R2 y configuración de seguridad CORS
Para que el navegador del cliente envíe solicitudes HTTP PUT directamente al dominio de Cloudflare R2, es necesario definir reglas de CORS (Cross-Origin Resource Sharing) en el bucket R2.
Vinculación del bucket R2 en wrangler.jsonc
// wrangler.jsonc
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "edge-r2-direct-upload",
"main": "src/index.ts",
"compatibility_date": "2026-01-01",
"compatibility_flags": ["nodejs_compat"],
// Vinculación de bucket Cloudflare R2
"r2_buckets": [
{
"binding": "MY_BUCKET",
"bucket_name": "effidev-media-uploads"
}
]
}
Configuración de la política CORS del bucket R2 (cors.json)
A través del comando wrangler r2 bucket cors set, se especifican los dominios de origen (Origin) permitidos y los métodos PUT.
[
{
"AllowedOrigins": ["https://effidev.dev", "http://localhost:3000"],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
"AllowedHeaders": ["Content-Type", "x-amz-*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3600
}
]
npx wrangler r2 bucket cors set effidev-media-uploads --file=cors.json
Paso 2: Implementación de la API de emisión de URLs pre-firmadas Serverless en el Edge (Hono.js)
Este es un servidor Hono.js configurado con una canalización compatible con Cloudflare Workers utilizando el SDK de AWS S3 para emitir URLs PUT pre-firmadas.
Instalación de paquetes
npm install hono @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
Handler de emisión de URLs pre-firmadas (src/index.ts)
// src/index.ts
import { Hono } from "hono";
import { cors } from "hono/cors";
import {
S3Client,
PutObjectCommand,
CreateMultipartUploadCommand,
UploadPartCommand,
CompleteMultipartUploadCommand,
} from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
type Env = {
Bindings: {
MY_BUCKET: R2Bucket;
R2_ACCOUNT_ID: string;
R2_ACCESS_KEY_ID: string;
R2_SECRET_ACCESS_KEY: string;
R2_BUCKET_NAME: string;
};
};
const app = new Hono<Env>();
app.use("*", cors());
// Fábrica de cliente compatible con S3 de Cloudflare R2
function getR2S3Client(env: Env["Bindings"]) {
return new S3Client({
region: "auto",
endpoint: `https://${env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: env.R2_ACCESS_KEY_ID,
secretAccessKey: env.R2_SECRET_ACCESS_KEY,
},
});
}
// -------------------------------------------------------------------
// 1. API de emisión de URL PUT pre-firmada para archivo único (hasta 100MB)
// -------------------------------------------------------------------
app.post("/api/upload/presigned-url", async (c) => {
const { fileName, contentType, fileSize } = await c.req.json();
// Validación de seguridad: límite máximo de 100MB
if (fileSize > 100 * 1024 * 1024) {
return c.json({ error: "File size exceeds 100MB limit. Use multipart upload." }, 400);
}
const s3Client = getR2S3Client(c.env);
const fileKey = `uploads/${Date.now()}_${crypto.randomUUID().slice(0, 8)}_${fileName}`;
const command = new PutObjectCommand({
Bucket: c.env.R2_BUCKET_NAME || "effidev-media-uploads",
Key: fileKey,
ContentType: contentType,
});
// Generación de firma de URL PUT pre-firmada válida por 5 minutos (300 segundos) (tarda 1ms)
const presignedUrl = await getSignedUrl(s3Client, command, { expiresIn: 300 });
return c.json({
success: true,
fileKey,
uploadUrl: presignedUrl,
expiresIn: 300,
});
});
Paso 3: API de carga multipart para archivos de gran tamaño (100MB ~ 5GB)
Para videos o datos binarios de gran tamaño, se aplica el método Multipart Upload, que divide los archivos en partes (chunks) para subirlos en paralelo y luego ensamblarlos.
// -------------------------------------------------------------------
// 2. Carga multipart de gran tamaño - Paso 1: Iniciar sesión multipart
// -------------------------------------------------------------------
app.post("/api/upload/multipart/initiate", async (c) => {
const { fileName, contentType } = await c.req.json();
const s3Client = getR2S3Client(c.env);
const fileKey = `videos/${Date.now()}_${fileName}`;
const command = new CreateMultipartUploadCommand({
Bucket: c.env.R2_BUCKET_NAME,
Key: fileKey,
ContentType: contentType,
});
const response = await s3Client.send(command);
return c.json({
uploadId: response.UploadId,
fileKey: fileKey,
});
});
// -------------------------------------------------------------------
// 3. Carga multipart de gran tamaño - Paso 2: Emitir URL pre-firmada para cada parte (Part)
// -------------------------------------------------------------------
app.post("/api/upload/multipart/presigned-part", async (c) => {
const { fileKey, uploadId, partNumber } = await c.req.json();
const s3Client = getR2S3Client(c.env);
const command = new UploadPartCommand({
Bucket: c.env.R2_BUCKET_NAME,
Key: fileKey,
UploadId: uploadId,
PartNumber: partNumber,
});
const uploadUrl = await getSignedUrl(s3Client, command, { expiresIn: 600 });
return c.json({ partNumber, uploadUrl });
});
// -------------------------------------------------------------------
// 4. Carga multipart de gran tamaño - Paso 3: Ensamblar y completar carga (Complete)
// -------------------------------------------------------------------
app.post("/api/upload/multipart/complete", async (c) => {
const { fileKey, uploadId, parts } = await c.req.json();
// Ejemplo de parts: [{ PartNumber: 1, ETag: '"e123..."' }, { PartNumber: 2, ETag: '"f456..."' }]
const s3Client = getR2S3Client(c.env);
const command = new CompleteMultipartUploadCommand({
Bucket: c.env.R2_BUCKET_NAME,
Key: fileKey,
UploadId: uploadId,
MultipartUpload: { Parts: parts },
});
await s3Client.send(command);
return c.json({
success: true,
fileUrl: `https://pub-effidev-media.r2.dev/${fileKey}`,
});
});
export default app;
Paso 4: Integración de Direct Upload en el cliente (Web / Flutter)
Ejemplo de integración JavaScript en cliente (navegador Web)
El cliente solo recibe uploadUrl desde el backend y luego realiza el envío directo con fetch(uploadUrl, { method: 'PUT', body: file }).
// Función de Carga Directa (Direct Upload) de archivo único en el frontend
async function uploadFileDirectToR2(file) {
// 1. Solicitud de URL firmada en 1ms al Worker
const res = await fetch("/api/upload/presigned-url", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
fileName: file.name,
contentType: file.type,
fileSize: file.size,
}),
});
const { uploadUrl, fileKey } = await res.json();
// 2. Transmisión HTTP PUT directa al bucket de Cloudflare R2 (¡sin retransmisión por backend!)
const uploadRes = await fetch(uploadUrl, {
method: "PUT",
headers: {
"Content-Type": file.type,
},
body: file, // Envío binario del objeto File / Blob
});
if (uploadRes.ok) {
console.log("R2 Direct Upload Success:", fileKey);
return fileKey;
} else {
throw new Error("Direct upload failed to R2 bucket");
}
}
Benchmark de rendimiento y costos en producción
Reporte comparativo entre el método tradicional de retransmisión por backend y Cloudflare R2 Presigned Direct Upload al procesar la carga de 10GB de archivos multimedia pesados (100 archivos de 100MB).
Tabla comparativa de rendimiento y costos
| Criterio de evaluación | Carga por retransmisión tradicional en backend | Cloudflare R2 Presigned Direct Upload | Efecto de mejora |
|---|---|---|---|
| Uso de CPU/RAM del servidor backend | 1,840 MB (Almacenamiento en búfer RAM) | 12 MB (Solo realiza la generación de firma) | 99.3% de reducción |
| Tasa de ocurrencia de timeouts en API | 8.4% (Interrupción por respuesta pesada de red) | 0.0% (El Worker completa en 1ms) | Eliminación del 100% de fallos |
| Velocidad de carga completada en cliente | 18.2 segundos | 8.4 segundos (Comunicación Directa 1:1) | 53.8% de mejora en velocidad |
| Costo de red Egress del backend | $0.09 / GB (Salida en AWS EC2/Lambda) | $0.00 / GB (Egress gratuito en R2) | 100% de ahorro en Egress |
| Costo mensual de mantenimiento (10TB) | $920.00 / mes | $0.01 / mes (Solo costo de almacenamiento Storage) | 99.9% de reducción de costos |
Conclusión: Arquitectura de archivos moderna sin pasar por el servidor
Ya no hay razón para desperdiciar memoria en los servidores backend, luchar contra los tiempos de espera ni sufrir por cargos excesivos de Egress de ancho de banda al procesar datos de archivos pesados.
La combinación de Cloudflare Workers y R2 Presigned URLs Direct Upload ofrece las siguientes ventajas decisivas:
- Consumo nulo de infraestructura de servidor: El Worker backend emite la URL pre-firmada en 1ms y finaliza inmediatamente, por lo que no hay consumo de memoria ni CPU.
- Costo de Egress de $0: Gracias a los beneficios de Cloudflare R2, el costo de tráfico de salida masivo es de $0.
- Escalabilidad ilimitada (Scalability): Dado que el cliente transmite directamente 1:1 al bucket edge de R2, no se genera carga en el servidor backend incluso si los usuarios simultáneos aumentan a decenas de miles.
- Soporte multipart de gran tamaño: Procesa en paralelo videos gigantescos de más de 5GB mediante una canalización en el edge.
Migra la lógica de carga por retransmisión del backend a R2 Presigned Direct Upload hoy mismo y experimenta una reducción de costos del 99% junto con una experiencia de usuario (UX) de carga en 1 segundo sin fallos.
Artículo relacionado: Consulta la guía de construcción de arquitectura R2 en Migración completa de AWS S3 a Cloudflare R2: Guía de Egress $0 y reducción del 95% en costos de procesamiento de imágenes.