Elido
15 min readtutorials
Cornerstone

Migrate from Bitly without breaking links: a failure-modes playbook

Seven classes of breakage when migrating from Bitly — slug case, DNS TTL, webhooks, deeplinks — and the audit script that catches them before users do

Ana Kowalska
Marketing solutions engineering
Migration flow diagram showing Bitly export feeding into Elido bulk-import with seven labeled checkpoints for breakage classes

Ana Kowalska is a solutions engineer at Elido who has walked a dozen migration projects through Bitly cutover. She thinks most of the pain is avoidable if you audit before you move rather than after.

The migrate-from-bitly-playbook covers the strategic arc: audit what you have, export it, import it, flip DNS. That post is the right place to start if you haven't done a Bitly migration before.

This post is different. It focuses on what breaks — the specific failure modes that emerge after the conceptual plan is done and you're actually running the migration against production data. Seven of them come up repeatedly, and all seven are preventable if you know to look.

TL;DR#

  • Bitly slugs are case-sensitive; many redirect platforms are not. bit.ly/AbCd and bit.ly/abcd are different links in Bitly and will behave differently if your migration script lowercases slugs on import.
  • DNS TTL gaps cause a redirect gap even after the CNAME flips. Drop TTL to 60 seconds at least 24 hours before cutover, not five minutes before.
  • Webhooks pointing at api-ssl.bitly.com endpoints stop firing the moment you cancel or deactivate the Bitly account. Re-wire every downstream consumer before you touch the account status.
  • Deeplinks with path segments (bit.ly/app/account/settings) collide with any Elido routing rules that also match on path prefix. Audit deeplink slugs separately from standard redirect slugs.

The seven things that actually break#

Before any tooling discussion, it helps to have the failure taxonomy in front of you. Most migration post-mortems point to one of these:

1. Slug case sensitivity. Bitly preserves case in slugs — bit.ly/SummerSale and bit.ly/summersale are distinct links. If your import script normalises slugs to lowercase (a common default in URL handling libraries), you silently create the wrong slug and the capital-letter variant 404s. This affects email campaigns where the slug was embedded with mixed case.

2. Trailing slash behaviour. bit.ly/campaign/ and bit.ly/campaign are handled as the same link in Bitly's router. Some platforms treat the trailing-slash variant as a distinct path. If your Elido workspace is fronted by a reverse proxy with strict URL normalisation enabled, a trailing-slash request may resolve differently than the canonical slug.

3. Query string preservation. If a Bitly link destination URL already contains query parameters — https://acme.example/landing?source=bitly — and the click also carries UTM parameters appended at share time, you need to verify that the destination merge behaves identically in Elido. Bitly's default behaviour for appended UTMs is to merge them into the existing query string. Test this explicitly for any link whose destination URL already carries parameters.

4. UTM appending at the platform level. Bitly's enterprise tier supports workspace-level UTM appending: every outgoing redirect gets a UTM appended regardless of what the original destination URL contains. If you had this enabled in Bitly and didn't document it, you may have analytics depending on UTMs that Elido isn't appending yet. Check your workspace settings in Bitly for auto-append rules before exporting. Elido's equivalent is UTM templates at the workspace or campaign level — the custom domains feature page covers where that configuration lives.

5. DNS TTL gap. This is the most common cause of a redirect gap at cutover. DNS resolvers cache the old CNAME for the duration of the current TTL. If your TTL has been sitting at 86400 seconds for two years, changing it to 300 seconds five minutes before you flip the A record means most resolvers still hold the old record for another 23 hours and 55 minutes. The cutover is not instant; it propagates.

6. Webhook re-wiring. Any system consuming Bitly webhook events — analytics pipelines, CRM enrichment jobs, Shopify order attribution — fires against the Bitly endpoint URL. That endpoint goes dark when you cancel or downgrade the Bitly account below the tier that supports webhooks. Bitly's webhook configuration lives at the account level and is not exported with the link data. Every consumer needs to be inventoried and re-pointed manually.

7. Deeplink path collisions. Mobile deeplinks often use the short URL path to encode app navigation state — bit.ly/app/profile/edit might map to a destination like yourapp://profile/edit. When you migrate these slugs to Elido, the slug app/profile/edit contains slashes. Elido's router may treat slash-delimited paths differently than Bitly's opaque slug treatment. Verify that deeplink slugs with path segments are created with the exact slug string, not re-interpreted as nested paths.


Pre-migration audit: segment by risk tier#

The Bitly API (accessed 2026-05-12) exposes per-link click counts via GET /v4/bitlink/{bitlink}/clicks/summary. Before you export and import anything, use this to segment your inventory.

The practical segmentation:

  • Must-not-break tier (top 1%): links with ≥10× the median click count in the last 30 days. These are live in emails, printed materials, paid ad landing pages. They need manual verification after cutover, not just automated checks.
  • Monitor tier (next 9%): links with above-median click volume. Automated 301 verification is sufficient, but flag any that resolve unexpectedly.
  • Bulk tier (remaining 90%): low-traffic or zero-traffic slugs. Verify programmatically; accept a small error rate and fix on report.

Export a 30-day click summary per link during the inventory step. A straightforward pagination loop against the Bitly API reference (accessed 2026-05-12) gives you this data; the link_clicks field in the group bitlinks endpoint is the lifetime counter, which is coarser but sufficient for triage:

# Paginate all links in a Bitly group and write to JSONL
NEXT_URL="https://api-ssl.bitly.com/v4/groups/${GROUP_GUID}/bitlinks?size=100"
while [ -n "$NEXT_URL" ]; do
  RESP=$(curl -s -H "Authorization: Bearer ${BITLY_TOKEN}" "$NEXT_URL")
  echo "$RESP" | jq -c '.links[]' >> bitly-links.jsonl
  NEXT_URL=$(echo "$RESP" | jq -r '.pagination.next // empty')
done

Sort the output by link_clicks descending. The top 1% is your must-not-break tier. Export their slugs to a separate file before running the bulk import.


Slug preservation: the import call that matters#

Elido's bulk import endpoint at POST /v1/links/bulk accepts a slug field per link. If you don't set it explicitly, Elido generates a new random slug — which is the wrong behaviour for a migration. Always pass the source slug.

# Bulk import with slug preservation — 100 links per call
curl -s -X POST "https://api.elido.app/v1/links/bulk" \
  -H "Authorization: Bearer ${ELIDO_API_KEY}" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: mig-batch-$(date +%s)" \
  -d '{
    "workspace_id": "'"${WORKSPACE_ID}"'",
    "domain_id": "'"${DOMAIN_ID}"'",
    "links": [
      {
        "slug": "SummerSale",
        "destination_url": "https://acme.example/summer",
        "tags": ["bitly-migrated", "campaign-summer"]
      },
      {
        "slug": "AbCd",
        "destination_url": "https://acme.example/landing",
        "tags": ["bitly-migrated"]
      }
    ]
  }'

Two things to notice in this call. First, the slug values are "SummerSale" and "AbCd" — mixed case preserved exactly as they appeared in Bitly. Do not lowercase them. Second, the Idempotency-Key header means you can re-run a partial batch safely; Elido returns the existing link rather than creating a duplicate. This is the correct pattern for a migration that may need to resume.

For the must-not-break tier, run the import interactively per link rather than in a batch, and verify each one before proceeding. For the bulk tier, batch at 100 per call and process the error array in the response to catch any slugs that conflicted or failed.


DNS cutover with no gap#

The DNS cutover is the moment where live traffic shifts. Done correctly, users experience no interruption. Done with a stale TTL, there is a gap measured in hours, not minutes.

The sequence matters. See the timeline diagram below.

Horizontal DNS cutover timeline showing T-7d TTL drop to 60s, T-0 A record flip, T+5min 95% propagation, T+1h TTL restore, T+24h audit pass

The timeline in detail:

T−7 days: Drop the TTL on the CNAME or A record to 60 seconds. This is the critical step that most teams miss. RFC 1034 §3.6 (IETF datatracker, section on resource record caching) defines TTL as the maximum cache duration a resolver may hold the record. If your current TTL is 86400 (one day), changing it only takes effect after the current cached version expires. You need to drop the TTL at least one full current-TTL-period before cutover. One week is safe; 24 hours is the minimum.

T−1 hour: Verify the low TTL has propagated. Use a tool like dig @8.8.8.8 links.yourbrand.com +ttl from at least three different resolver endpoints. The reported TTL should be near 60 seconds.

T−0: Swap the CNAME target from Bitly's edge to Elido's edge. On Elido's side, the domain should already be registered and verified in your workspace — do not flip DNS before Elido's edge is ready to accept the traffic. The first request after propagation triggers Caddy's on-demand TLS certificate issuance, which completes in roughly 2-3 seconds on that one request. Subsequent requests hit the cache and resolve in single-digit milliseconds at the Frankfurt edge.

T+5 minutes: Run a spot-check from a second network (use a mobile hotspot to bypass your office resolver's cache). curl -sI https://links.yourbrand.com/any-known-slug should return a 301 Moved Permanently pointing at the expected destination, sourced from Elido's edge headers.

T+1 hour: Restore the TTL to its normal operational value (300 or 3600 seconds). Keeping the TTL at 60 seconds indefinitely adds load to your DNS provider and resolver infrastructure.

T+24 hours: Run the full slug audit (see the next section).

Per RFC 7231 §6.4.2, a 301 Moved Permanently response may be cached by intermediaries indefinitely unless an explicit cache-control header overrides it. This means any client that hit the old Bitly destination during the TTL gap may have cached a 301 pointing at Bitly's infrastructure. These cached redirects resolve correctly as long as Bitly's infrastructure is live, which is why the 30-day Bitly account overlap window matters.


The 301 chain audit: scripted nightly verification#

After cutover, run a nightly verification loop over your must-not-break tier. The goal is to catch any slug that changed behaviour — either returning an unexpected destination, returning a 404, or growing a redirect chain longer than two hops.

# Verify top slugs resolve correctly via Elido
# top-slugs.txt: one slug per line, no protocol prefix
DOMAIN="links.yourbrand.com"
FAIL=0

while IFS= read -r slug; do
  STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
    --max-redirs 0 \
    "https://${DOMAIN}/${slug}")
  LOCATION=$(curl -s -I --max-redirs 0 \
    "https://${DOMAIN}/${slug}" \
    | grep -i '^location:' | tr -d '\r' | cut -d' ' -f2)

  if [ "$STATUS" != "301" ]; then
    echo "FAIL [$STATUS] $slug → expected 301"
    FAIL=$((FAIL + 1))
  else
    echo "OK  [301] $slug$LOCATION"
  fi
done < top-slugs.txt

echo "---"
echo "Failures: $FAIL"
exit $FAIL

Run this against the must-not-break tier (typically 50–200 slugs for most teams) every night for the first two weeks post-cutover. Pipe the output to your alerting channel. If FAIL is non-zero, you want a human to look at it before morning traffic peaks.

The --max-redirs 0 flag is intentional: you want Elido's redirect, not the final destination. If the Status is 200 instead of 301, something on Elido's side is serving the destination directly rather than redirecting, which means the slug resolved to a link configured as a direct pass-through. That is worth investigating.

For the monitor tier, run a lighter weekly scan. For the bulk tier, rely on error reports from downstream systems — broken links in emails generate bounce-rate changes that your email platform will surface.


Webhook re-wiring#

Bitly webhooks are documented at the Bitly API reference (accessed 2026-05-12) under the Webhooks section. Each webhook fires on click events and includes the bitlink, referrer, and user-agent fields. Common consumers:

  • Shopify: attribution apps that track which short link drove a conversion. Configured in the Shopify app's admin panel, pointing at a third-party endpoint that calls Bitly's webhook verification.
  • Stripe: some billing attribution pipelines tag incoming trial signups with the UTM data from the originating short link, sourced via the Bitly webhook.
  • Slack: link performance bots that post click summaries to a #marketing channel.
  • Custom ETL pipelines: any data warehouse pipeline that ingests Bitly click events for enrichment or attribution joins.

The migration checklist for webhooks:

  1. Export your Bitly webhook configuration before any account changes. The Bitly API GET /v4/workspaces/{workspace_guid}/webhooks returns the list. Save it to a file.
  2. For each consumer, identify the endpoint URL receiving Bitly events and the secret used for HMAC verification.
  3. Set up the equivalent Elido webhook endpoint. Elido's webhook payloads have a different schema than Bitly's — the fields are similar but not identical. Adjust the consumer's handler to accept the new schema.
  4. Run both webhooks in parallel during the overlap window. Configure Elido to fire webhooks starting at cutover day, while keeping the Bitly webhook active. Your consumer receives two events per click during overlap — deduplicate on the short URL + timestamp, or accept double-counting during the overlap window as a known artifact.
  5. After 72 hours of confirmed Elido webhook delivery, remove the Bitly webhook configuration from each consumer.

The secret-rotation grace window is the overlap period. Do not rotate the Elido webhook secret until every consumer has been verified. Rotating the secret before one consumer is updated means that consumer silently drops events without an error — the HMAC check fails and most webhook handlers discard invalid-signature payloads without alerting.


Rollback plan: keep Bitly alive for 30 days#

The rollback procedure is simple: revert the DNS CNAME to Bitly's target. Because you pre-staged the TTL drop and the DNS record is still at 60 seconds (until you restore it), a DNS revert propagates in under two minutes.

Stage the rollback command before you start:

# Rollback script — run this to revert DNS to Bitly (adapt for your DNS provider)
# Route 53 example using AWS CLI
aws route53 change-resource-record-sets \
  --hosted-zone-id "${HOSTED_ZONE_ID}" \
  --change-batch '{
    "Changes": [{
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "links.yourbrand.com",
        "Type": "CNAME",
        "TTL": 60,
        "ResourceRecords": [{"Value": "cname.bitly.com"}]
      }
    }]
  }'

Keep this in a file on your laptop and in a shared runbook location before cutover. The worst moment to be writing infrastructure commands is during an active incident.

Keep the Bitly account active and on a paid plan that maintains link resolution for 30 days post-cutover. The migrate-from-bitly-playbook recommends 90 days; 30 is the practical minimum for teams that need to control costs. During the 30-day window, any traffic that still resolves via Bitly (cached redirects, old bit.ly links in printed materials) continues to work. After 30 days, evaluate the residual Bitly traffic in your analytics and decide whether to extend.

What to monitor during the 30-day window:

  • Elido's error rate on your custom domain (watch for unexpected 404s in the access log).
  • Any spikes in traffic to Bitly (the Bitly dashboard shows traffic; a spike may mean a cached redirect is still resolving through Bitly for a high-volume slug).
  • Webhook consumer error rates for any consumers you re-wired.

Post-migration audit: what to log#

After the 30-day window, run a final audit pass. What belongs in the audit log:

CheckMethodPass criterion
Slug count matcheswc -l bitly-export.jsonl vs Elido API countWithin 1% (account for intentionally dropped archived links)
Must-not-break tier 301 checkNightly audit scriptZero failures for 7 consecutive days
Click volume reconciliationCompare Elido 30-day click total vs Bitly 30-day total from same period last yearWithin expected seasonal variance
Webhook consumer confirmationVerify each consumer is receiving Elido events and processing correctlyNo silent drops for 7 days
DNS TTL restoreddig +ttl links.yourbrand.comTTL at operational value (300+ seconds)

Log this in your team's audit table. If your workspace is on a Business or Enterprise plan, Elido's audit log captures all API operations during the import and is queryable via the API. Pull the import batch records and store a snapshot alongside this table.


Common gotchas: three patterns from the field#

The DACH ecommerce brand that lost a week of email attribution. A retailer in Germany ran a newsletter campaign using Bitly slugs with per-subscriber UTMs appended at send time. The migration script normalised all slugs to lowercase before importing them into Elido. Post-cutover, the email platform was generating links with the original mixed-case slugs. Those links returned 404 from Elido because the slug case didn't match. The fix was to re-run the import with case-preserved slugs, but by then seven days of email traffic had landed on 404s. Attribution was unrecoverable for that cohort. The lesson: test one live link from each active channel before declaring the migration complete.

The SaaS startup that triple-redirected mobile users. A growth team had a Bitly custom domain fronted by Cloudflare in proxy (orange-cloud) mode. Post-cutover, mobile users were getting three redirects: Cloudflare → Elido edge → destination. The extra hop came from a Cloudflare Page Rule that rewrote HTTP to HTTPS before handing off to Elido, then Elido issued its own 301. iOS Safari cached the intermediate Cloudflare redirect as a permanent redirect for 30 days. The fix was to set the Cloudflare record to grey-cloud (DNS-only) and remove the conflicting Page Rule. The cached redirects in Safari took 30 days to expire naturally. Verify your CDN proxy mode before cutover, not after.

The agency that missed a Bitly group. An agency managed three client brands under a single Bitly account, each under a different Bitly group with its own custom domain. The migration script queried only the default group — the one the API user's token was created under. Two client domains migrated cleanly. The third, under a secondary group, was never exported. Post-cutover, a product launch email campaign went out pointing at the unmigrated custom domain. The domain still had Bitly's CNAME at full TTL, and Bitly was serving the links correctly — but the cutover window for that domain had been declared done. The scramble was a full re-migration under deadline. The lesson: enumerate all groups via GET /v4/user/groups before starting any export step. Check that the token has access to every group.


Where to go from here#

The migrate-from-bitly-playbook covers the full strategic sequence for teams starting from scratch on migration planning. This post is the failure-modes companion — use them together.

For the product side of what you're migrating to, the solutions/marketers page covers the attribution and campaign tracking features that most migration projects are trying to gain access to. The /compare/vs-bitly page is the feature parity reference if you're still confirming that the switch is worth it.

If you're evaluating Elido alongside Rebrandly or Short.io, the elido-vs-bitly comparison covers pricing and feature trade-offs at four traffic volumes. The custom domains feature page documents the DNS verification and TLS provisioning mechanics in detail — worth reading before your DNS cutover window.

Migration failures are almost always avoidable. The audit script, the TTL discipline, and the webhook inventory take two hours of work before cutover. They save days of incident response after it.


Citations and sources

Try Elido

EU-hosted URL shortener with custom domains, deep analytics, and an open API. Free tier — no credit card.

Tags
migrate from bitly
bitly migration
bitly export
url migration
dns cutover
301 redirect

Continue reading

Migrate from Bitly without breaking links: a failure-modes playbook · Elido