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 modeTester limitApproval neededBuild expiryUse case
Internal Testing100 users (App Store Connect members)None — instant90 daysDogfooding inside your company
External Testing10,000 usersFirst build of each version reviewed (~24h)90 daysPublic/customer betas
Public LinkSame external pool, no email neededYes, first build per version90 daysMarketing-driven beta signups
Ad Hoc100 devices/year per device typeNone1 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

  1. “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.
  2. “Public Link bypasses beta review.” No — public link distribution still requires beta review on the first build of each version.
  3. “TestFlight expiry can be extended.” No. 90 days, hard cap. Plan release cadence around it.
  4. “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.
  5. “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