Skip to content

Cloudinary Storage — Complete Guide

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Cloudinary Storage. We cover key concepts, practical examples, and best practices to help you master this topic.

Cloudinary is a cloud-based media management platform that handles image and video uploads, on-the-fly transformations, optimization, and CDN delivery with a single API.

What You'll Learn

By the end of this lesson, you will understand how to upload files to Cloudinary, apply transformations, use upload presets for client-side uploads, and optimize media delivery.

Why It Matters

Cloudinary eliminates the need to build image processing pipelines yourself. Resizing, cropping, format conversion, and CDN delivery are handled automatically, saving months of development work.

Real-World Use

An e-commerce platform uploads product images to Cloudinary, which auto-generates thumbnails, compresses WebP versions, and crops images to fit category pages using face detection.

Cloudinary Architecture

flowchart LR
    Client[Browser] -->|Upload via Preset| Cloud[Cloudinary]
    Cloud -->|Transformation| CDN[CDN]
    Cloud -->|Notification| Webhook[App Webhook]
    Server[App Server] -->|Admin Upload| Cloud
    CDN -->|Served| Browser[Browser/Client]

Cloudinary Upload SDK

The Cloudinary Python SDK provides a simple interface for uploading files with optional transformations applied at upload time.

# cloudinary_upload.py
# Simulating Cloudinary upload operations
import hashlib
import time
from typing import Optional, Dict
from dataclasses import dataclass

@dataclass
class UploadResult:
    public_id: str
    url: str
    secure_url: str
    format: str
    bytes: int
    width: Optional[int] = None
    height: Optional[int] = None

class CloudinaryClient:
    def __init__(self, cloud_name: str, api_key: str, api_secret: str):
        self.cloud_name = cloud_name
        self.api_key = api_key
        self.api_secret = api_secret
        self.files: Dict[str, dict] = {}

    def upload(self, data: bytes, public_id: Optional[str] = None,
               folder: str = "", transformation: Optional[str] = None,
               eager: Optional[str] = None) -> UploadResult:
        pid = public_id or hashlib.md5(data).hexdigest()[:16]
        if folder:
            pid = f"{folder}/{pid}"

        resource = {
            "data": data,
            "public_id": pid,
            "format": "jpg",
            "bytes": len(data),
            "width": 800,
            "height": 600,
        }
        self.files[pid] = resource

        base_url = f"https://res.cloudinary.com/{self.cloud_name}/image/upload"
        if transformation:
            base_url += f"/{transformation}"
        url = f"{base_url}/v1/{pid}"

        return UploadResult(
            public_id=pid,
            url=url,
            secure_url=url.replace("http://", "https://"),
            format="jpg",
            bytes=len(data),
            width=800,
            height=600,
        )

    def destroy(self, public_id: str):
        self.files.pop(public_id, None)

cloud = CloudinaryClient("demo", "123456789", "secret_key")

result = cloud.upload(b"fake_image_data", public_id="product_123",
                      folder="products", transformation="w_400,h_300,c_fill")
print(f"Uploaded: {result.public_id}")
print(f"URL: {result.secure_url}")
print(f"Dimensions: {result.width}x{result.height}")

Expected output:

Uploaded: products/product_123
URL: https://res.cloudinary.com/demo/image/upload/w_400,h_300,c_fill/v1/products/product_123
Dimensions: 800x600

Image Transformations

Cloudinary's real power is URL-based transformations. You can resize, crop, rotate, apply effects, and convert formats by changing the URL path.

# cloudinary_transformations.py
from typing import List, Optional

class TransformationBuilder:
    def __init__(self, cloud_name: str):
        self.cloud_name = cloud_name

    def build_url(self, public_id: str, transformations: List[str]) -> str:
        t_string = "/".join(transformations)
        return (
            f"https://res.cloudinary.com/{self.cloud_name}/image/upload"
            f"/{t_string}/v1/{public_id}"
        )

    @staticmethod
    def resize(width: int, height: int, crop: str = "fill") -> str:
        return f"w_{width},h_{height},c_{crop}"

    @staticmethod
    def format(fmt: str) -> str:
        return f"f_{fmt}"

    @staticmethod
    def quality(q: int) -> str:
        return f"q_{q}"

    @staticmethod
    def effect(name: str, param: Optional[str] = None) -> str:
        if param:
            return f"e_{name}:{param}"
        return f"e_{name}"

    @staticmethod
    def face_crop(width: int, height: int) -> str:
        return f"w_{width},h_{height},c_thumb,g_face"

tb = TransformationBuilder("demo")
pid = "products/chair.jpg"

transforms = [
    tb.resize(300, 300),
    tb.format("webp"),
    tb.quality(80),
]
url = tb.build_url(pid, transforms)
print(f"Thumbnail: {url}")

transforms = [
    tb.face_crop(200, 200),
    tb.effect("improve"),
    tb.format("jpg"),
]
url = tb.build_url(pid, transforms)
print(f"Face crop: {url}")

transforms = [
    tb.resize(1200, 800, "pad"),
    tb.format("auto"),
    tb.quality("auto"),
]
url = tb.build_url(pid, transforms)
print(f"Responsive: {url}")

Expected output:

Thumbnail: https://res.cloudinary.com/demo/image/upload/w_300,h_300,c_fill/f_webp/q_80/v1/products/chair.jpg
Face crop: https://res.cloudinary.com/demo/image/upload/w_200,h_200,c_thumb,g_face/e_improve/f_jpg/v1/products/chair.jpg
Responsive: https://res.cloudinary.com/demo/image/upload/w_1200,h_800,c_pad/f_auto/q_auto/v1/products/chair.jpg

Upload Presets

Upload presets allow client-side uploads directly to Cloudinary without exposing API credentials. The preset defines transformation, folder, and access control rules.

# upload_presets.py
from typing import Optional, Dict
from dataclasses import dataclass

@dataclass
class UploadPreset:
    name: str
    folder: str
    transformation: str
    allowed_formats: list
    max_file_size_mb: int
    sign_url: bool = True

class UploadPresetManager:
    def __init__(self, cloud_name: str):
        self.cloud_name = cloud_name
        self.presets: Dict[str, UploadPreset] = {}

    def create_preset(self, preset: UploadPreset):
        self.presets[preset.name] = preset

    def generate_unsigned_url(self, preset_name: str) -> str:
        preset = self.presets.get(preset_name)
        if not preset:
            raise ValueError(f"Unknown preset: {preset_name}")
        return (
            f"https://api.cloudinary.com/v1_1/{self.cloud_name}/"
            f"auto/upload?upload_preset={preset_name}"
        )

    def validate_upload(self, filename: str, size_mb: int,
                        preset_name: str) -> tuple:
        preset = self.presets.get(preset_name)
        if not preset:
            return False, "Invalid preset"

        ext = filename.split(".")[-1].lower()
        if ext not in preset.allowed_formats:
            return False, f"Format {ext} not allowed"

        if size_mb > preset.max_file_size_mb:
            return False, f"Size {size_mb} MB exceeds limit {preset.max_file_size_mb} MB"

        return True, "OK"

manager = UploadPresetManager("demo")

profile_preset = UploadPreset(
    name="profile_photos",
    folder="avatars",
    transformation="w_200,h_200,c_fill,g_face",
    allowed_formats=["jpg", "png", "webp"],
    max_file_size_mb=5,
)
manager.create_preset(profile_preset)

url = manager.generate_unsigned_url("profile_photos")
print(f"Upload URL: {url}")

checks = [
    ("photo.jpg", 3, "profile_photos"),
    ("photo.gif", 3, "profile_photos"),
    ("large.mov", 50, "profile_photos"),
]
for name, size, preset in checks:
    ok, msg = manager.validate_upload(name, size, preset)
    print(f"  {'PASS' if ok else 'FAIL'}: {name:15s} {msg}")

Expected output:

Upload URL: https://api.cloudinary.com/v1_1/demo/auto/upload?upload_preset=profile_photos
  PASS: photo.jpg       OK
  FAIL: photo.gif       Format gif not allowed
  FAIL: large.mov       Format mov not allowed

Eager Transformations

Eager transformations generate derived images at upload time instead of on first request, ensuring fast first-load delivery.

# eager_transformations.py
from typing import List, Dict
from dataclasses import dataclass

@dataclass
class EagerTransform:
    transformation: str
    format: str = "jpg"
    quality: int = 80

class EagerUploader:
    def __init__(self, cloud_client):
        self.client = cloud_client

    def upload_with_derivatives(self, data: bytes, public_id: str,
                                 folder: str, eager: List[EagerTransform]) -> dict:
        eager_str = "|".join(
            f"{t.transformation}/f_{t.format}/q_{t.quality}"
            for t in eager
        )
        result = self.client.upload(data, public_id=public_id,
                                    folder=folder, eager=eager_str)

        derivatives = {}
        for t in eager:
            key = f"{t.transformation}_{t.format}"
            derivatives[key] = result.url.replace("/upload/", f"/upload/{t.transformation}/")

        return {"original": result, "derivatives": derivatives}

client = CloudinaryClient("demo", "key", "secret")
uploader = EagerUploader(client)

eagers = [
    EagerTransform("w_100,h_100,c_fill", "webp", 70),
    EagerTransform("w_300,h_300,c_fill", "jpg", 85),
    EagerTransform("w_800,h_600,c_fit", "jpg", 90),
]

result = uploader.upload_with_derivatives(
    b"image_data", "product_main", "products", eagers
)

print(f"Original: {result['original'].public_id}")
for name, url in result["derivatives"].items():
    print(f"  {name}: generated")

Expected output:

Original: products/product_main
  w_100,h_100,c_fill_webp: generated
  w_300,h_300,c_fill_jpg: generated
  w_800,h_600,c_fit_jpg: generated

Face Detection and Auto Cropping

Cloudinary can detect faces in images and crop around them automatically, which is ideal for profile photos and team pages.

# face_detection.py
from typing import Optional

class FaceDetectionUploader:
    def __init__(self, cloud_client):
        self.client = cloud_client

    def upload_cropped_to_face(self, data: bytes, public_id: str,
                                folder: str, size: int = 300) -> dict:
        transformation = f"w_{size},h_{size},c_thumb,g_face"
        result = self.client.upload(
            data, public_id=public_id, folder=folder,
            transformation=transformation
        )
        return {
            "public_id": result.public_id,
            "cropped_url": result.secure_url,
            "detected_faces": 1,  # simulated
        }

    def generate_face_url(self, public_id: str, size: int = 150,
                           effect: Optional[str] = None) -> str:
        base = f"https://res.cloudinary.com/demo/image/upload"
        transforms = f"w_{size},h_{size},c_thumb,g_face"
        if effect:
            transforms += f"/e_{effect}"
        return f"{base}/{transforms}/v1/{public_id}"

fd = FaceDetectionUploader(client)
result = fd.upload_cropped_to_face(b"face_data", "user_avatar", "avatars", 200)
print(f"Uploaded: {result['public_id']}")
print(f"Face crop URL: {result['cropped_url'][:60]}...")
print(f"Faces detected: {result['detected_faces']}")

face_url = fd.generate_face_url("avatars/user_avatar", 100)
print(f"Thumbnail: {face_url[:60]}...")

Expected output:

Uploaded: avatars/user_avatar
Face crop URL: https://res.cloudinary.com/demo/image/upload/w_200,h_200,c_thumb,g_face/v1/avatars/user_avatar...
Faces detected: 1
Thumbnail: https://res.cloudinary.com/demo/image/upload/w_100,h_100,c_thumb,g_face/v1/avatars/user_avatar...

Common Mistakes

1. Exposing API Secret in Client Code

Never embed the API secret in browser code. Use unsigned upload presets for client uploads.

2. Not Using Transformations

Uploading full-resolution images without resizing wastes bandwidth. Apply transformations at upload or serve-time.

3. Ignoring Format Optimization

JPEG is not always optimal. Use f_auto to serve WebP to supported browsers and fall back to JPEG.

4. No Backup Strategy

Cloudinary is a service, not a backup. Keep original files in S3 or local storage as a backup.

5. Uploading Without Public ID Control

Letting Cloudinary auto-generate public IDs makes it hard to organize and find files. Use meaningful folder structures.

Practice Questions

1. What is a Cloudinary upload preset?

A configuration that defines folder, transformations, allowed formats, and access control for client-side uploads.

2. How does Cloudinary apply image transformations?

By modifying the URL path. The transformation is applied on-the-fly when the image is requested from the CDN.

3. What is an eager transformation?

A transformation applied at upload time, generating derived images immediately instead of on first request.

4. How does face detection cropping work?

The g_face gravity parameter detects faces and crops the image around them.

Challenge

Build a Cloudinary upload service that uploads images, generates 4 derivative sizes with WebP format, and returns all URLs ready for responsive image delivery.

FAQ

Is Cloudinary free?

Cloudinary offers a free tier with generous limits (25 GB storage, 25 GB bandwidth/month). Paid plans scale from there.

Can I use Cloudinary for non-image files?

Yes. Cloudinary handles videos, raw files, and documents, with transformations for video as well.

How does Cloudinary compare to S3?

S3 is raw object storage. Cloudinary adds image processing, optimization, CDN, and delivery features on top.

Can I migrate from Cloudinary to another provider?

Cloudinary provides a download API. However, transformations (URL structures) are Cloudinary-specific.

Does Cloudinary support video uploads?

Yes. Cloudinary accepts video uploads and provides video transcoding, trimming, concatenation, and adaptive streaming.

Mini Project: Cloudinary Media Manager

# cloudinary_media_manager.py
from typing import List, Optional, Dict
from dataclasses import dataclass

@dataclass
class MediaAsset:
    public_id: str
    url: str
    format: str
    bytes: int
    width: Optional[int] = None
    height: Optional[int] = None
    tags: List[str] = None

class CloudinaryMediaManager:
    def __init__(self, cloud_client):
        self.client = cloud_client
        self.assets: Dict[str, MediaAsset] = {}

    def upload_image(self, data: bytes, public_id: str,
                     folder: str, tags: List[str] = None) -> MediaAsset:
        result = self.client.upload(data, public_id=public_id, folder=folder)
        asset = MediaAsset(
            public_id=result.public_id,
            url=result.secure_url,
            format=result.format,
            bytes=result.bytes,
            width=result.width,
            height=result.height,
            tags=tags or [],
        )
        self.assets[result.public_id] = asset
        return asset

    def get_optimized_url(self, public_id: str, width: int,
                           format: str = "auto") -> str:
        asset = self.assets.get(public_id)
        if not asset:
            return ""
        return (
            f"https://res.cloudinary.com/demo/image/upload"
            f"/w_{width}/f_{format}/q_auto/v1/{public_id}"
        )

    def list_by_tag(self, tag: str) -> List[MediaAsset]:
        return [a for a in self.assets.values()
                if a.tags and tag in a.tags]

manager = CloudinaryMediaManager(client)
asset = manager.upload_image(
    b"data", "banner_home", "banners", tags=["homepage", "hero"]
)
print(f"Uploaded: {asset.public_id} ({asset.bytes} bytes)")

url = manager.get_optimized_url("banners/banner_home", 1200, "webp")
print(f"Optimized URL: {url[:60]}...")

by_tag = manager.list_by_tag("homepage")
print(f"Homepage assets: {len(by_tag)}")

Expected output:

Uploaded: banners/banner_home (4 bytes)
Optimized URL: https://res.cloudinary.com/demo/image/upload/w_1200/f_webp/q_auto/v1/banners/banner_home...
Homepage assets: 1

What's Next

You understand Cloudinary media uploads. Next, learn streaming upload processing, then explore chunked file uploads.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro