The Rotary Club of Calcutta Central Gardens needed a production event photo management system. Admins upload high-resolution images from a Keystatic CMS dashboard; the photos get stored in Cloudinary and served from their CDN. Standard setup. Official Node SDK. Followed the docs exactly.
Local development: flawless. Deployed to Vercel: HTTP 500 every time an admin tried to upload anything.
The error: Must supply cloud_name.
What the docs say to do
The Cloudinary SDK provides a convenience configuration method:
import { v2 as cloudinary } from 'cloudinary';
cloudinary.config(true);
config(true) is supposed to automatically read the CLOUDINARY_URL environment variable — a single connection string in the format cloudinary://api_key:api_secret@cloud_name — and parse it into the SDK's internal configuration. Set that one env var and you're done.
It works in Express and Node.js CLI scripts. It does not reliably work in Next.js 15 App Router serverless route handlers on Vercel.
What's actually happening
In serverless environments, each request spawns an isolated execution context. Module-level initialization code (like cloudinary.config(true)) runs during cold starts in an environment where Vercel's runtime isn't always done mapping all environment variables into process.env at the exact moment that line executes — or where the SDK's internal URI parser has issues with special characters in API secrets.
Whatever the exact failure mode (and honestly, different requests fail differently), the end result is the same: cloudinary.config().cloud_name comes back undefined. Every upload request throws an unhandled exception.
What doesn't fix it
Split environment variables: You can break the connection string into separate CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, and CLOUDINARY_API_SECRET vars and configure them explicitly. This works but breaks any external tooling that expects to read a single CLOUDINARY_URL string.
Hardcoding credentials: No.
Retrying config(true) on each request: The SDK holds the config in a module-level singleton. Calling config(true) multiple times doesn't help if the first call failed silently and set all values to undefined.
The actual fix: a regex fallback parser
The configuration string format is deterministic: cloudinary://key:secret@cloudname. If the SDK fails to parse it, parse it yourself.
import { v2 as cloudinary } from 'cloudinary';
cloudinary.config(true);
const currentConfig = cloudinary.config();
if (!currentConfig.cloud_name && process.env.CLOUDINARY_URL) {
try {
const url = process.env.CLOUDINARY_URL;
const matches = url.match(/cloudinary:\/\/([^:]+):([^@]+)@(.+)/);
if (matches) {
cloudinary.config({
api_key: matches[1],
api_secret: matches[2],
cloud_name: matches[3],
});
}
} catch (err) {
console.error('Failed to manually parse CLOUDINARY_URL:', err);
}
}
export default cloudinary;
export { cloudinary };
After config(true) runs, check currentConfig.cloud_name. If it's falsy and CLOUDINARY_URL exists in the environment, run the URI through a regex capture group: group 1 is the API key, group 2 is the API secret, group 3 is the cloud name. Inject them explicitly with cloudinary.config({...}).
The SDK is happy. The uploads work. The admins never know any of this happened.
This went in as commit 3e4250a — "file upload bettered".
The broader principle
Third-party SDK convenience methods are convenient for standard environments. They break in edge cases — serverless cold starts, Turbopack compilation, unusual environment variable injection timing. For any critical infrastructure dependency (cloud storage, databases, payment gateways), always inspect the resulting configuration object after initialization. If it looks wrong, don't trust the SDK to retry or self-heal — provide your own fallback parsing.
cloudinary.config(true) looks like a one-liner that just works. Treat it like any other network operation: verify it succeeded before proceeding, and have a fallback ready if it didn't.