10.4 — TestFlight
Opening scenario
You ship a TestFlight build on a Friday for your weekend beta group. Saturday morning, a tester messages: “App won’t open — says ‘this beta has expired’.” You check: the build was uploaded 91 days ago. TestFlight builds expire after 90, no warning, no grace period. Your beta cohort, 200 strong, is now locked out until you upload a new build, wait for processing, and re-distribute.
TestFlight is the fastest way to get pre-release builds in real hands — but it has constraints, expiry rules, and review steps you have to plan around.
Context taxonomy
| Distribution mode | Tester limit | Approval needed | Build expiry | Use case |
|---|---|---|---|---|
| Internal Testing | 100 users (App Store Connect members) | None — instant | 90 days | Dogfooding inside your company |
| External Testing | 10,000 users | First build of each version reviewed (~24h) | 90 days | Public/customer betas |
| Public Link | Same external pool, no email needed | Yes, first build per version | 90 days | Marketing-driven beta signups |
| Ad Hoc | 100 devices/year per device type | None | 1 year (cert/profile validity) | Off-store distribution for QA, contractors |
Concept → Why → How → Code
Concept. TestFlight is an Apple-hosted, throttled distribution channel layered onto App Store Connect. Same upload pipeline, lighter review, time-boxed.
Why. Apple wants beta apps tested by real users on real devices but doesn’t want indefinite “off-store” distribution that bypasses normal review. The 90-day expiry forces a beta cadence.
How — the lifecycle.
1. Build & upload (Xcode / Fastlane pilot / Transporter)
↓
2. Apple processes build (5–30 minutes); status changes from
"Processing" → "Ready to Submit" (internal) or
"Waiting for Beta Review" (external)
↓
3. Add Beta App Description, Email, Feedback URL (per version, external only)
↓
4. Submit for Beta Review (external only) — first build per version
↓
5. Apple Beta Review (~24h) → Approved
↓
6. Distribute to groups (Internal / External)
↓
7. Users install via TestFlight app, submit feedback
↓
8. ~Day 80 — TestFlight emails users that build expires soon
↓
9. Day 90 — Build expires. Upload a new one or lose your testers.
Upload a build with Fastlane pilot.
# Fastfile
lane :beta do
build_app(
scheme: "Acme",
export_method: "app-store"
)
upload_to_testflight(
api_key_path: "fastlane/AuthKey_AAAA1111BB.json",
skip_waiting_for_build_processing: false,
distribute_external: true,
groups: ["Beta Cohort A"],
changelog: File.read("CHANGELOG.md"),
beta_app_review_info: {
contact_email: "beta@acme.com",
contact_first_name: "Pat",
contact_last_name: "Reviewer",
contact_phone: "+1 555 0100",
demo_account_name: "demo@acme.com",
demo_account_password: ENV["DEMO_PASSWORD"],
notes: "Use demo account; tap 'Trips' to see core flow."
}
)
end
Manage testers via the App Store Connect API.
# Invite an external tester to a group
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
https://api.appstoreconnect.apple.com/v1/betaTesters \
-d '{
"data": {
"type": "betaTesters",
"attributes": {
"email": "user@example.com",
"firstName": "User",
"lastName": "Name"
},
"relationships": {
"betaGroups": {
"data": [{"type": "betaGroups", "id": "'$GROUP_ID'"}]
}
}
}
}'
itmstransporter (Transporter CLI) — legacy but useful as a fallback when Xcode/Fastlane can’t upload.
xcrun iTMSTransporter -m upload \
-assetFile build/Acme.ipa \
-apiKey $API_KEY_ID \
-apiIssuer $ISSUER_ID
In the wild
- Marco Arment ships Overcast betas to a tight internal group of ~15 power users every couple of weeks — relies on TestFlight’s feedback screenshot annotations.
- Slack uses Internal Testing for engineers and External Testing for customer-success-selected enterprise customers (under 10k cap, no public link).
- Linear runs three external beta groups: “Alpha” (~50), “Beta” (~500), “Beta Wide” (~5000) — each gets a build a day apart so regressions surface in alpha before reaching wide.
- Crypto exchanges and other high-risk apps often skip TestFlight entirely and use Ad Hoc + MDM because TestFlight feedback is visible to Apple’s reviewers.
Common misconceptions
- “Internal testers don’t need beta review.” True for internal groups, but you still need to upload a build that compiles — Apple’s processing pipeline catches malformed binaries even for internal distribution.
- “Public Link bypasses beta review.” No — public link distribution still requires beta review on the first build of each version.
- “TestFlight expiry can be extended.” No. 90 days, hard cap. Plan release cadence around it.
- “Beta testers see the App Store listing.” They see a TestFlight listing with the Beta App Description — not the App Store description. Don’t confuse the two.
- “All 10k tester slots are free.” Yes. The cap is the only constraint. No per-tester charge.
Seasoned engineer’s take
TestFlight is your most underrated production tool. Use it for:
- Phased canary: ship to internal first (5–10 power users), then alpha (~50), then external wide. Bake for 24–48h at each stage.
- Crash regression detection: TestFlight crashes are visible in App Store Connect alongside symbolicated stack traces, often hours before real users would surface them.
- Feature flagging: ship a build with flags off, enable them for a beta group via remote config, measure, then enable for App Store.
TIP. Always include a meaningful changelog in
upload_to_testflight. Without it, testers won’t know what to test — and TestFlight feedback quality plummets. Treat changelogs as a contract: “if I test this, you tested this.”
WARNING. Beta App Review notes are read by humans. Vague notes (“test the app”) cause rejections. Include a demo account, a tour script, and a phone number you actually answer.
The strategic move: when a build is ~70 days old, set up CI to push a maintenance build automatically (v1.2.3-beta-refresh) just to reset the expiry clock for long-running QA cohorts.
Interview corner
Junior — “What’s the difference between TestFlight Internal and External testing?” Internal: up to 100 App Store Connect users, no review, instant distribution. External: up to 10,000 users by email or public link, requires beta review on the first build of each version, ~24h turnaround.
Mid — “A beta build expired with active testers — how do you recover and prevent it next time?” Recover by uploading a new build and re-distributing — same CFBundleShortVersionString, incremented build number. Prevent by setting a CI alarm at day 70 to auto-push a refresh build, and by tagging long-running QA cohorts so you know which versions to keep alive.
Senior — “Design a TestFlight strategy for a 1M-DAU consumer app shipping weekly.” Three external groups: Alpha (internal employees + power users, 200), Wide Beta (5,000 opt-ins via public link), QA (100 contractors on Ad Hoc as fallback). CI pushes Monday → Alpha, Wednesday → Wide if no crashes, Friday → App Store submission. Each step gates on TestFlight crash-free-session-rate ≥ 99.5% measured from the previous wave. All builds tagged in Git with the TestFlight cohort.
Red flag — “We just push every commit to TestFlight.” You’ll hit Apple’s processing throttles, drown testers in updates, and burn beta reviewer goodwill (Apple notices when you submit 30 builds a week).
Lab preview
Lab 10.1 builds a Fastlane lane that uploads to TestFlight with a baked-in changelog, demo account, and group routing — the production pattern in 30 lines.
Next: 10.5 — Fastlane