The Problem: Twitter’s 280-Character Limit
When sharing technical blog posts on Twitter, I constantly hit the 280-character limit. Long URLs consume precious space that should be used for actual content. For example:
This leaves barely enough room for a meaningful tweet. Third-party URL shorteners like Bitly work, but they:
- Cost money for custom domains ($29/month for Bitly Pro)
- Don’t give you full control over your data
- May inject their own analytics or tracking
- Could shut down and break all your links
The Solution: Cloudflare Workers
Cloudflare Workers is a serverless platform that runs your code at the edge, across Cloudflare’s global network. Combined with KV (Key-Value) storage, it’s perfect for building a URL shortener.
Why Cloudflare Workers?
- ✅ Free Tier: 100,000 requests/day
- ✅ Global: <15ms response time worldwide
- ✅ Custom Domain: Use your own domain
- ✅ No Servers: Fully managed, auto-scaling
- ✅ Simple: JavaScript/TypeScript code
Architecture Overview
The system has three main components:
Implementation
Step 1: Worker Code
Create worker.js with the main logic:
export default {
async fetch(request, env) {
const url = new URL(request.url);
// Handle short URL redirects
if (url.pathname.startsWith('/s/')) {
return handleRedirect(url, env);
}
// Handle API: create short URL
if (url.pathname === '/api/shorten') {
return handleCreate(request, env);
}
// Handle API: get statistics
if (url.pathname.startsWith('/api/stats/')) {
return handleStats(url, env);
}
return new Response('Not Found', { status: 404 });
}
};
async function handleRedirect(url, env) {
const code = url.pathname.split('/s/')[1];
// Get mapping from KV
const mapping = await env.URL_MAPPINGS.get(code, { type: 'json' });
if (!mapping) {
return new Response('Short URL not found', { status: 404 });
}
// Build target URL with UTM parameters
const targetUrl = new URL(mapping.url);
const source = url.searchParams.get('s') || 'short';
targetUrl.searchParams.set('utm_source', source);
targetUrl.searchParams.set('utm_medium', 'shortlink');
targetUrl.searchParams.set('utm_campaign', mapping.campaign || 'general');
// Track stats asynchronously
trackAccess(code, source, env);
// 301 permanent redirect
return Response.redirect(targetUrl.toString(), 301);
}
async function handleCreate(request, env) {
// Verify API key
const authHeader = request.headers.get('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return new Response('Unauthorized', { status: 401 });
}
const apiKey = authHeader.substring(7);
if (apiKey !== env.API_KEY) {
return new Response('Invalid API key', { status: 401 });
}
const body = await request.json();
const { url: longUrl, code, campaign } = body;
// Generate or use provided short code
const shortCode = code || generateCode();
// Store in KV
await env.URL_MAPPINGS.put(shortCode, JSON.stringify({
url: longUrl,
campaign: campaign || 'general',
created: new Date().toISOString()
}));
return Response.json({
shortUrl: `https://example.com/s/${shortCode}`,
code: shortCode,
longUrl
});
}
function generateCode() {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let code = '';
for (let i = 0; i < 6; i++) {
code += chars[Math.floor(Math.random() * chars.length)];
}
return code;
}
async function trackAccess(code, source, env) {
const statsKey = `stats:${code}`;
const stats = await env.URL_MAPPINGS.get(statsKey, { type: 'json' }) || {
total: 0,
sources: {}
};
stats.total++;
stats.sources[source] = (stats.sources[source] || 0) + 1;
stats.lastAccess = new Date().toISOString();
await env.URL_MAPPINGS.put(statsKey, JSON.stringify(stats));
}
Step 2: Configuration
Create wrangler.toml:
name = "url-shortener"
main = "worker.js"
compatibility_date = "2024-01-01"
[[kv_namespaces]]
binding = "URL_MAPPINGS"
id = "YOUR_KV_NAMESPACE_ID"
[[routes]]
pattern = "yourdomain.com/s/*"
zone_name = "yourdomain.com"
[[routes]]
pattern = "yourdomain.com/api/shorten"
zone_name = "yourdomain.com"
[[routes]]
pattern = "yourdomain.com/api/stats/*"
zone_name = "yourdomain.com"
Step 3: Deploy
# Install Wrangler CLI
npm install -g wrangler
# Login to Cloudflare
wrangler login
# Create KV namespace
wrangler kv:namespace create URL_MAPPINGS
# Set API key (for authentication)
wrangler secret put API_KEY
# Deploy
wrangler deploy
Python Client Integration
Create a Python client for easy integration:
import os
import requests
from typing import Optional
class URLShortener:
def __init__(self, api_key: Optional[str] = None):
self.api_key = api_key or os.getenv('SHORTENER_API_KEY')
self.api_endpoint = 'https://yourdomain.com/api/shorten'
def create(self, long_url: str, code: Optional[str] = None,
campaign: str = 'blog_post') -> Optional[str]:
if not self.api_key:
return None
try:
response = requests.post(
self.api_endpoint,
json={
'url': long_url,
'code': code,
'campaign': campaign
},
headers={'Authorization': f'Bearer {self.api_key}'},
timeout=10
)
if response.status_code == 200:
return response.json()['shortUrl']
except Exception as e:
print(f'Error creating short URL: {e}')
return None
def shorten_url_for_twitter(long_url: str) -> str:
"""Create short URL with automatic fallback"""
shortener = URLShortener()
short_url = shortener.create(long_url)
# Fallback to simplified UTM if shortening fails
return short_url if short_url else f'{long_url}?utm_source=twitter'
Usage Example
from url_shortener import shorten_url_for_twitter
# Original URL (115 characters)
url = 'https://example.com/posts/building-self-hosted-url-shortener-cloudflare-workers/'
# Shortened URL (32 characters)
short = shorten_url_for_twitter(url)
# Result: https://example.com/s/abc123
# Character savings: 83 characters (72% reduction)
print(f'Saved {len(url) - len(short)} characters!')
Results
Before and after comparison for Twitter:
| URL Type | Length | Available for Content |
|---|---|---|
| Full URL + UTM | 155 chars | 125 chars (44%) |
| Short URL | 32 chars | 248 chars (88%) ✨ |
98% increase in available space for content!
GitHub Actions Integration
Automate short URL creation in your CI/CD pipeline:
name: Auto Publish
on:
push:
branches: [main]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup URL Shortener
run: |
echo "SHORTENER_API_KEY=${{ secrets.SHORTENER_API_KEY }}" >> $GITHUB_ENV
- name: Publish to Twitter
run: |
python3 publish.py --use-short-urls
Cost Analysis
Cloudflare Workers Free Tier:
- 100,000 requests/day
- 1GB KV storage
- Unlimited bandwidth
Estimated usage for a blog:
- ~500 short URL clicks/day
- ~5 new URLs created/day
- ~1MB KV storage used
Cost: $0/month (well within free tier)
Comparison:
- Bitly Pro: $29/month
- TinyURL Pro: $9.99/month
- Self-hosted: $0/month ✨
Performance
Tested from different locations:
Thanks to Cloudflare’s edge network, responses are blazing fast worldwide.
Security Considerations
- API Authentication: Creating short URLs requires API key
- Public Access: Redirects work without authentication
- Anonymous Stats: No IP addresses or personal data stored
- Rate Limiting: Built-in via Cloudflare
Monitoring
View real-time metrics in Cloudflare Dashboard:
- Request count
- Error rate
- P50/P95/P99 latency
- KV read/write operations
Advanced Features
Custom Short Codes
Create memorable short URLs:
shortener.create(
long_url='https://example.com/posts/oauth-guide/',
code='oauth' # Custom code
)
# Result: https://example.com/s/oauth
Click Analytics
Track where your clicks come from:
curl https://example.com/api/stats/abc123
Response:
{
"total": 142,
"sources": {
"twitter": 98,
"linkedin": 32,
"direct": 12
},
"lastAccess": "2025-11-27T10:30:00Z"
}
Conclusion
Building a self-hosted URL shortener with Cloudflare Workers provides:
✅ Complete control over your URLs ✅ Zero cost (free tier is generous) ✅ Global performance (<15ms worldwide) ✅ Custom domain (your brand) ✅ Privacy (no third-party tracking) ✅ Reliability (99.99% uptime SLA)
For Twitter and other character-limited platforms, this solution provides 98% more space for content compared to full URLs.
The complete implementation takes under 5 minutes to deploy and is production-ready. Perfect for developers, bloggers, and anyone sharing links on social media.
Resources
Happy link shortening! 🚀