CFBundleVersion Rules: Why Uploads Get Rejected
CFBundleVersion is the Info.plist key that stores your iOS build number, and Apple says it must be a string of one to three period-separated integers using only digits and periods. App Store Connect combines it with the user-visible version to identify each upload, so a build number you have already used for that version gets the upload refused with error ITMS-90189. If you inherited an app, the version scheme is one of the first things worth reading, because a confusing one slows every release that follows.

The pages that rank for this term fall into three groups. There are forum threads that answer one error message, there is a single good write-up of how the two version keys differ, and there are bug reports from tools that generate Info.plist files. None of them that I read connects the number to the situation that actually costs teams a release day, which is picking up a project where nobody remembers how the numbers were chosen. This article covers the rules Apple documents, then the practical side: where the number comes from, how CI and cross-platform frameworks change it, and how to repair a scheme you didn’t design. RapidLabs does this kind of cleanup on inherited apps, so the focus here is on what to verify before you ship, not on theory.
What is CFBundleVersion, and how is it different from the version number?
CFBundleVersion is the build number, and CFBundleShortVersionString is the version your customers see. Apple’s documentation defines CFBundleShortVersionString as the user-visible version of the bundle and CFBundleVersion as the version of the build, an iteration of that bundle that may or may not have been released. App Store Connect shows them together, so version 1.2.1 build 42 appears as 1.2.1 (42).
The practical difference is who reads each one. The short version string appears on your App Store page, in the Settings screen of your own app if you display it, and in release notes. The build number mostly lives in App Store Connect, TestFlight, crash reports and support tickets, where it tells you exactly which binary a tester or a customer was running.
That second job is the reason the number matters after launch. When a crash report says build 18445 and you can map that to a commit, you know what code produced it. When the number was set by hand and reused, the same report points at several different binaries and your debugging starts with a guess.
What format does CFBundleVersion accept?
It accepts one to three integers separated by periods, with digits and periods as the only allowed characters. Apple’s reference page gives 10.14.1 as the example and describes the three integers as major, minor and patch. You can write fewer than three, and Apple says the missing ones count as zeros, so 0 means 0.0.0, 10 means 10.0.0 and 10.5 means 10.5.0.
Apple also says you can include more integers but that the system ignores them. That has one consequence worth taking seriously. If somebody on your team used a fourth segment to tell builds apart, for example 1.2.1.7 and 1.2.1.8, you shouldn’t assume the system treats those two as different builds, because it is documented to ignore anything past the third integer.
The one-integer form is the simplest and the one most teams settle on. A plain counter such as 42, then 43, then 44 has no format to get wrong, it sorts correctly, and it works in every tool in the chain from Xcode to the App Store Connect API. If your project uses the three-integer form for the build number, check that the pattern isn’t trying to carry meaning, such as encoding the date, that nobody can decode a year later.
Why does App Store Connect say the build was already uploaded?
The error is ITMS-90189, Redundant Binary Upload, and it appears when you upload a build whose build number was already used for that version number. The message reads along the lines of “You’ve already uploaded a build with build number ‘18445’ for version number ‘3.66.0’”, followed by an instruction to increment the build string before you upload again. Apple’s App Store Connect help explains why, saying that the bundle ID and version number associate a build with an app and a version record, and that the build string uniquely identifies the build throughout the system.

The fix in nearly every case is to raise CFBundleVersion to a number you have never used for that version, and to archive again. The part that costs time is finding where the number is set, which the next sections cover, because Xcode will happily archive the old number again if the value is coming from somewhere you didn’t edit.
There is one wrinkle from Apple’s own forum. In a thread about this exact error, an App Store Connect engineer said that if it happened within the past day or so it might be a problem on Apple’s side, and suggested raising the version number and uploading another build. That is a single engineer’s reply to a single case, so treat it as a reason to try a different number before spending an hour debugging, not as a known fault you can rely on.
Before you change anything, check which builds App Store Connect actually holds. Open the app, go to the TestFlight tab, and look at the build list for that version. A build uploaded from a previous contractor’s laptop and forgotten is the usual source of a surprise collision.
Does the build number have to increase, or only be unique?
On iOS it only has to be unique within a version, and on macOS it has to keep rising across versions. Apple states this directly in its Xcode Cloud documentation. For iOS, iPadOS, tvOS, visionOS and watchOS apps, App Store Connect requires each app version to use a unique combination of the version string and the build number, so Apple’s own example moves from 1.2.1 (42) to 1.2.2 (1) without a problem. For Mac apps, Apple says the build number must keep increasing even across app versions, which makes 1.2.2 (1) invalid after 1.2.1 (42) and 1.2.2 (43) valid.
So a counter that restarts for every release is legal on iPhone. I still wouldn’t recommend it for a team that ships often, and the reason is the paper trail rather than the rule. Two different builds that both say (3) exist in your history, and any log or crash report that carries only the build number is ambiguous until you also look up the version.
A number that only goes up is easier to live with. It orders your builds in time, it fits one column in a spreadsheet, and it never needs the sentence “which version was that again?” I would take that over saving a few digits. If the existing project already restarts per release, don’t rewrite history to fix it, just stop doing it from the next build onward by picking a start value above anything you’ve ever uploaded.
Where does the number come from in your project?
It comes from one of three places, and on an inherited project it is often coming from a different one than you’d expect. The first is the Build field on the General tab of the target in Xcode, which writes the CURRENT_PROJECT_VERSION build setting. The second is a hard-coded value in the Info.plist file. The third is a script or CI step that overwrites either of the first two at build time.

Apple’s Technical Q&A QA1827 describes how the command-line tool agvtool fits in. It says agvtool searches project.pbxproj for CURRENT_PROJECT_VERSION, that the value must be an integer or a floating point number such as 34.6, and that it also looks in your Info.plist for the version keys and updates them if they exist. It adds that setting the Versioning System to Apple Generic makes sure Xcode includes the agvtool-generated version information in the project.
For a takeover, this suggests a short investigation before you touch anything. Open the target’s Info.plist and see whether CFBundleVersion holds a literal number or a variable such as $(CURRENT_PROJECT_VERSION). Then search the project for CURRENT_PROJECT_VERSION in every configuration, because a Release configuration that sets its own value will quietly ignore the one you changed on the General tab.
Also look at every target, not just the main one, since app extensions such as widgets carry their own copy of the version keys and each one needs a deliberate value.
How do CI builds and Xcode Cloud change the number?
They can overwrite it, and that is usually the intent. A CI system that stamps its own build counter into the project means the number in your repository is no longer the number that ships, which surprises the next developer who edits it by hand and sees no effect.
Xcode Cloud is a clear example. Apple says it assigns a build number to each build, an integer that starts at 1 and increases with every build, and that App Store Connect uses that number when you distribute through TestFlight or release. Apple also says Xcode Cloud build numbers must always be integers, so hashes, timestamps and other strings aren’t accepted. When you start using Xcode Cloud on an existing iOS app, the first build gets number 1, which is fine for iOS because only the combination of version and build has to be unique. For an existing Mac app that is a problem, since the number has to rise across versions.

Apple’s fix for that is a setting in App Store Connect. Under the Xcode Cloud tab you choose Settings, then Build Number, and edit the Next Build Number field, and Apple says only members with the Admin or App Manager role can set a custom value. If your team moved from a different CI system to Xcode Cloud, this is the setting that stops the new counter from colliding with numbers the old one already used.
The same logic applies to any hosted CI. Before you trust the number in the repository, look at the pipeline definition and find the step that sets the build number, whether it reads a counter, a commit count or a timestamp. Whichever it is, write the rule down in the repository’s README so the next person doesn’t have to rediscover it from an upload error.
How do Flutter, React Native and Expo set it?
Each framework has its own source of truth that eventually writes CFBundleVersion, so the number you edit may not be the one the framework uses. The table below shows where to look for the three most common setups.
| Framework | Version users see | Build number | Maps to CFBundleVersion |
|---|---|---|---|
| Flutter | Part before the plus sign in pubspec.yaml | Part after the plus sign, or —build-number | Yes |
| Expo (EAS) | version in app config | ios.buildNumber | Yes |
| Native Xcode | CFBundleShortVersionString | CFBundleVersion | Itself |
Flutter’s documentation says the pubspec version is three numbers separated by dots, followed by an optional build number separated by a plus sign, as in 1.0.0+1. It also says both can be overridden when running flutter build ipa with —build-name and —build-number, and that on iOS build-name uses CFBundleShortVersionString while build-number uses CFBundleVersion. A CI script that passes —build-number will therefore beat whatever is written in pubspec.yaml.
Expo’s documentation describes two sources for the developer-facing number. With the remote version source, which Expo says is the recommended behavior from EAS CLI version 12.0.0, the EAS servers manage the build versions and autoIncrement raises them on each build. With the local source, your project is the source of truth and Expo doesn’t write to it, so you must commit the change after each build. Expo also warns that autoIncrement doesn’t support the version property and isn’t compatible with the nativeVersion runtime version policy for EAS Update.
React Native without Expo is the plain native case. The number lives in the Xcode project and in Android’s Gradle file, so edit both files by hand or script them, and keep that in mind when you plan a version jump like the one described in our React Native upgrade article.
Can Android and iOS share one build number?
They can share it only if the value stays inside what Android allows. Android’s versionCode is a positive integer, and Google’s documentation says the greatest value Google Play accepts is 2,100,000,000 and that each successive release must use a greater value. The Android system uses it to stop users from installing an APK with a lower versionCode than the one already on the device, which means a number that goes backwards is rejected on that platform for good.
The versionName is the Android counterpart of the short version string. Google describes it as the string displayed to users and the only value they see, and it can be any identifier you like, which is looser than the strict three-integer rule on iOS.
The trap in shared schemes is the timestamp. A build number such as 202610071530, built from year, month, day, hour and minute, is fine for CFBundleVersion because it is all digits. It is also roughly one hundred times larger than the Android limit of 2,100,000,000, so the first Android release that tries to reuse it fails. A date-only stamp like 20261007 fits, but then two builds on the same day collide, and you are back to the problem the stamp was meant to avoid.
If an inherited project uses one number for both platforms, check whether the Android side has already climbed close to the limit. A plain counter that has run for years is nowhere near it, but a number derived from the clock may already be unusable, and you’d rather learn that during planning than during a release. The same ownership questions come up when a Play listing changes hands, which our article on Google Play app transfer covers.
What should you check first on an app whose numbers you didn’t choose?
Start by finding the highest build number App Store Connect has ever seen for the app, because that sets the floor for everything you’ll do. Look through the TestFlight build list and the version history, note the largest value, and pick your next build number comfortably above it. A gap costs nothing, since nobody sees the build number but your team, and a collision costs you a release.
Then read how the number gets written today, using the investigation above, and decide on one rule. My recommendation for most inherited apps is a single integer that only ever increases, set by the CI system and never edited by hand, with the user-visible version changed only when you cut a real release. That rule works with agvtool, Xcode Cloud, Fastlane-style scripts and the Flutter and Expo flags, so it doesn’t lock you into one tool.
Finally, write the rule where the next developer will see it, and make one test upload to TestFlight before you need the number for real. If the app also has unclear ownership of its Apple account, sort that out first, because you can’t fix a number you can’t upload under, and our notes on Apple app transfer explain what tends to break there. A rejected binary is a different problem from a duplicate number, and the App Store rejection reasons article is the place to tell them apart.
If you are taking over an app and the release process is the part nobody can explain, that’s the situation the RapidLabs app takeover service is built for, and the RapidLabs maintenance audit covers the version scheme along with the rest of the release pipeline. Either way, the version number is a small thing to fix once and an expensive thing to rediscover on every release.
Frequently asked questions
What is CFBundleVersion in an iOS app?
CFBundleVersion is the Info.plist key that holds your build number, the value that identifies one specific build of the app. Apple describes it as a string of one to three period-separated integers, such as 10.14.1, containing only digits and periods. In Xcode you normally see it as the Build field on the General tab, and App Store Connect shows it in brackets after the version, for example 1.2.1 (42).
What is the difference between CFBundleVersion and CFBundleShortVersionString?
CFBundleShortVersionString is the user-visible version, which Apple says must be three period-separated integers such as 10.14.1. CFBundleVersion is the build number that identifies an iteration of that version, released or not. Your customers see the first one on the App Store page, and only you and your testers see the second one in App Store Connect and TestFlight.
How do I fix the error that I've already uploaded a build with this build number?
The error is ITMS-90189, and it means App Store Connect already holds a build with that exact build number for that version number. Raise CFBundleVersion to a value you have never uploaded and archive again. One Apple engineer has said that if the error appears for a number you are sure is new, it can be an issue on Apple's side, and that raising the version number and uploading again is worth trying.
Does the iOS build number have to keep increasing?
For iOS, iPadOS, tvOS, visionOS and watchOS apps, Apple says App Store Connect requires each app version to use a unique combination of the version string and the build number, so a new version can start again at a lower build number. Mac apps are stricter, and Apple says their build number must keep increasing even across versions. Many teams still make the number rise forever on every platform because it avoids ever having to think about it.
Can the Android versionCode and the iOS build number be the same number?
They can, but only if the number fits both systems. Google Play allows a versionCode of at most 2,100,000,000, while iOS only needs digits and periods. A timestamp such as 202610071530 works on iOS but is far above the Android limit, so a shared scheme built on long timestamps will eventually break the Android release.
Can I use a build number with letters in it, like 42-beta?
Not in the build number, because Apple states that CFBundleVersion can only contain numeric characters and periods, so a suffix such as -beta or a commit hash is not allowed. Xcode Cloud goes further and only accepts plain integers as build numbers, so hashes and timestamps are ruled out there. Keep the label in your release notes or your branch name and keep the build number numeric.
Have a product decision to make?
RapidLabs helps founders and operators shape, build, and launch focused software.
Email the studio