Overview
Proper streaming configuration is critical for low-latency audio playback. This guide covers optimizing Traefik for audio streaming, particularly addressing mobile buffering issues.
Stream URL Configuration
La Urban uses the following stream configuration:
const CONFIG = {
STREAM_URL: 'https://azura.laurban.cl/listen/laurban/media' ,
API_URL: 'https://azura.laurban.cl/api/nowplaying/laurban' ,
// ...
};
HTML Audio Element
The audio element is configured with:
< audio id = "audio"
name = "media"
preload = "none"
crossorigin = "anonymous" >
Tu navegador no soporta el elemento < code > audio </ code > .
</ audio >
Key attributes:
preload="none": Prevents auto-buffering, reduces initial load time
crossorigin="anonymous": Enables CORS for Web Audio API
name="media": Identifies the media element
Common Streaming Issues
Problem: Mobile Buffering Delay
Symptom : Desktop plays almost immediately (~1-2s), but iPhone/mobile takes 5-10 seconds before audio starts.
Root Cause : Traefik (or other reverse proxies) attempting to buffer the entire response before sending to the client. Since audio streams are infinite, this creates significant delays.
Impact on mobile devices:
✅ Desktop: Reproduces in ~1-2s
❌ iPhone: Takes 5-10s to start playing
❌ Lyrics become out of sync even with delay compensation
Traefik Optimization for Streaming
Key Configuration Changes
The solution involves three critical configurations:
Disable buffering completely
Add streaming-specific headers
Configure flush intervals for real-time data transfer
1. No-Buffer Middleware
This middleware tells Traefik to not buffer responses, which is essential for streaming infinite audio data.
# Critical: Disable all buffering for streaming
traefik.http.middlewares.azuracast-nobuffer.buffering.maxRequestBodyBytes=0
traefik.http.middlewares.azuracast-nobuffer.buffering.maxResponseBodyBytes=0
traefik.http.middlewares.azuracast-nobuffer.buffering.memRequestBodyBytes=0
traefik.http.middlewares.azuracast-nobuffer.buffering.memResponseBodyBytes=0
traefik.http.middlewares.azuracast-nobuffer.buffering.retryExpression=IsNetworkError() && Attempts() < 2
Why this matters:
maxResponseBodyBytes=0: Don’t buffer response (critical for streams)
memResponseBodyBytes=0: Don’t use memory buffering
Traefik sends data immediately to client instead of waiting
Add these headers to prevent caching and buffering at all proxy layers:
# Anti-buffering header
traefik.http.middlewares.azuracast-cors.headers.customresponseheaders.X-Accel-Buffering=no
# Anti-cache headers
traefik.http.middlewares.azuracast-cors.headers.customresponseheaders.Cache-Control=no-cache, no-store, must-revalidate
traefik.http.middlewares.azuracast-cors.headers.customresponseheaders.Pragma=no-cache
traefik.http.middlewares.azuracast-cors.headers.customresponseheaders.Expires=0
Header explanations:
Explicit instruction to reverse proxies (Traefik, Nginx) to not buffer the response. Critical for real-time streaming.
Cache-Control: no-cache, no-store
Prevents browsers and intermediaries from caching the stream. Ensures users always get fresh audio data.
Legacy cache control header for older browsers and proxies.
3. Flush Interval Configuration
# Send data every 100ms instead of waiting for buffer to fill
traefik.http.services.azuracast-stream-8000-service.loadbalancer.responseForwarding.flushInterval=100ms
traefik.http.services.azuracast-stream-8010-service.loadbalancer.responseForwarding.flushInterval=100ms
Effect : Traefik sends data every 100ms, ensuring minimal latency and smooth streaming.
Complete Traefik Configuration
Docker Compose Labels
Here’s the complete optimized configuration for AzuraCast with Traefik:
services :
azuracast :
labels :
# Network and basic settings
- "traefik.docker.network=localnet"
- "traefik.enable=true"
# ==========================================
# MIDDLEWARE: CORS (Enhanced)
# ==========================================
- "traefik.http.middlewares.azuracast-cors.headers.accesscontrolallowcredentials=false"
- "traefik.http.middlewares.azuracast-cors.headers.accesscontrolexposeheaders=Content-Length,Content-Range,Icy-Br,Icy-Description,Icy-Genre,Icy-MetaInt,Icy-Name,Icy-Pub,Icy-Url,X-Accel-Buffering"
- "traefik.http.middlewares.azuracast-cors.headers.accesscontrolmaxage=3600"
- "traefik.http.middlewares.azuracast-cors.headers.addvaryheader=true"
- "traefik.http.middlewares.azuracast-cors.headers.customresponseheaders.Access-Control-Allow-Headers=*"
- "traefik.http.middlewares.azuracast-cors.headers.customresponseheaders.Access-Control-Allow-Methods=GET,HEAD,OPTIONS"
- "traefik.http.middlewares.azuracast-cors.headers.customresponseheaders.Access-Control-Allow-Origin=*"
- "traefik.http.middlewares.azuracast-cors.headers.customresponseheaders.Access-Control-Max-Age=3600"
# Critical streaming headers
- "traefik.http.middlewares.azuracast-cors.headers.customresponseheaders.X-Accel-Buffering=no"
- "traefik.http.middlewares.azuracast-cors.headers.customresponseheaders.Cache-Control=no-cache, no-store, must-revalidate"
- "traefik.http.middlewares.azuracast-cors.headers.customresponseheaders.Pragma=no-cache"
- "traefik.http.middlewares.azuracast-cors.headers.customresponseheaders.Expires=0"
# ==========================================
# MIDDLEWARE: NO BUFFERING (Critical!)
# ==========================================
- "traefik.http.middlewares.azuracast-nobuffer.buffering.maxRequestBodyBytes=0"
- "traefik.http.middlewares.azuracast-nobuffer.buffering.maxResponseBodyBytes=0"
- "traefik.http.middlewares.azuracast-nobuffer.buffering.memRequestBodyBytes=0"
- "traefik.http.middlewares.azuracast-nobuffer.buffering.memResponseBodyBytes=0"
- "traefik.http.middlewares.azuracast-nobuffer.buffering.retryExpression=IsNetworkError() && Attempts() < 2"
# ==========================================
# ROUTER: Port 8000 (main stream)
# ==========================================
- "traefik.http.routers.azuracast-stream-8000.entrypoints=stream-8000"
- "traefik.http.routers.azuracast-stream-8000.middlewares=azuracast-nobuffer,azuracast-cors"
- "traefik.http.routers.azuracast-stream-8000.rule=Host(`stream.laurban.cl`)"
- "traefik.http.routers.azuracast-stream-8000.service=azuracast-stream-8000-service"
- "traefik.http.routers.azuracast-stream-8000.tls=true"
- "traefik.http.routers.azuracast-stream-8000.tls.certresolver=tlsresolver"
# ==========================================
# SERVICES: Load balancers with flush interval
# ==========================================
- "traefik.http.services.azuracast-stream-8000-service.loadbalancer.server.port=8000"
- "traefik.http.services.azuracast-stream-8000-service.loadbalancer.responseForwarding.flushInterval=100ms"
- "traefik.http.services.azuracast-stream-8010-service.loadbalancer.server.port=8010"
- "traefik.http.services.azuracast-stream-8010-service.loadbalancer.responseForwarding.flushInterval=100ms"
Docker Compose Override Method
For easier updates without modifying the main docker-compose.yml, create docker-compose.override.yml:
version : '3.8'
services :
azuracast :
labels :
# Enhanced CORS
- "traefik.http.middlewares.azuracast-cors.headers.accesscontrolmaxage=3600"
- "traefik.http.middlewares.azuracast-cors.headers.customresponseheaders.X-Accel-Buffering=no"
- "traefik.http.middlewares.azuracast-cors.headers.customresponseheaders.Cache-Control=no-cache, no-store, must-revalidate"
- "traefik.http.middlewares.azuracast-cors.headers.customresponseheaders.Pragma=no-cache"
- "traefik.http.middlewares.azuracast-cors.headers.customresponseheaders.Expires=0"
# No-buffer middleware
- "traefik.http.middlewares.azuracast-nobuffer.buffering.maxRequestBodyBytes=0"
- "traefik.http.middlewares.azuracast-nobuffer.buffering.maxResponseBodyBytes=0"
- "traefik.http.middlewares.azuracast-nobuffer.buffering.memRequestBodyBytes=0"
- "traefik.http.middlewares.azuracast-nobuffer.buffering.memResponseBodyBytes=0"
# Apply to routers
- "traefik.http.routers.azuracast-stream-8000.middlewares=azuracast-nobuffer,azuracast-cors"
- "traefik.http.routers.azuracast-stream-8010.middlewares=azuracast-nobuffer,azuracast-cors"
# Flush intervals
- "traefik.http.services.azuracast-stream-8000-service.loadbalancer.responseForwarding.flushInterval=100ms"
- "traefik.http.services.azuracast-stream-8010-service.loadbalancer.responseForwarding.flushInterval=100ms"
Before and After Comparison
Without Optimization
iPhone → Traefik → [BUFFER 8-10s] → Safari receives data → Play
^^^^^^^^^^^^^
PROBLEM HERE
Result:
Desktop: ~1-2s ✅
iPhone: ~8-10s ❌
Poor user experience on mobile
With Optimization
iPhone → Traefik → [100ms flush] → Safari receives data → Play
^^^^^^^^^^^^
REAL STREAMING
Result:
Desktop: ~1-2s ✅
iPhone: ~2-3s ✅
Consistent experience across devices
Verification and Testing
Apply Changes
Update configuration
Add the labels to your docker-compose.yml or create docker-compose.override.yml
Restart containers
docker-compose down
docker-compose up -d
Verify Traefik applied changes
docker logs traefik | grep azuracast
# Should respond immediately, not wait
time curl -v https://stream.laurban.cl:8000/media | head -c 1000
# Expected time: < 1 second
Check that proper headers are being sent:
curl -v https://stream.laurban.cl:8000/media 2>&1 | grep -i "x-accel\|cache-control"
Expected output:
< X-Accel-Buffering: no
< Cache-Control: no-cache, no-store, must-revalidate
< Icy-MetaInt: 16000
Open DevTools → Network → Select the stream request → Headers:
Must have:
X-Accel-Buffering: no
Cache-Control: no-cache, no-store, must-revalidate
Transfer-Encoding: chunked ← Important: chunked streaming
Must NOT have:
Content-Length: [number] ← If present, buffering is occurring
Expected Results
With these optimizations applied:
✅ iPhone : Playback starts in 2-3s (vs 8-10s before)
✅ Desktop : Remains fast at ~1-2s
✅ Synchronized lyrics : 4.5s delay is now accurate
✅ No interruptions : Continuous stream without reconnections
✅ Better UX : Users won’t think the player is broken
Important Notes
Don’t apply no-buffer to web interfaces : The azuracast-nobuffer middleware is only for streaming endpoints . Do NOT add it to web UI routes or API endpoints.
Apply nobuffer ONLY to:
azuracast-stream-8000
azuracast-stream-8010
azuracast-stream-path
Do NOT apply to:
azuracast-web (web interface)
API endpoints
Static pages
Troubleshooting
Still experiencing delays?
Check Traefik logs for errors
docker logs traefik 2>&1 | grep -i "buffer\|timeout"
Verify no Content-Length header
Clear mobile browser cache completely and test in private/incognito mode. iOS Safari is particularly aggressive with caching.
Audio Element Configuration
The JavaScript code configures the audio element for optimal streaming:
// Set stream URL
const CONFIG = {
STREAM_URL: 'https://azura.laurban.cl/listen/laurban/media' ,
UPDATE_INTERVAL: 5000 ,
INITIAL_DELAY: 500 ,
// ...
};
// Configure audio element
if ( ! audio . src || audio . src === '' || audio . src === window . location . href ) {
audio . src = CONFIG . STREAM_URL ;
}
// Direct play without preloading
await audio . play ();
Mobile Optimizations
The player includes specific mobile optimizations:
function isMobileDevice () {
return /Android | webOS | iPhone | iPad | iPod | BlackBerry | IEMobile | Opera Mini/ i . test ( navigator . userAgent ) ||
( navigator . maxTouchPoints && navigator . maxTouchPoints > 2 );
}
const isMobile = isMobileDevice ();
const updateInterval = isMobile ? 80 : 50 ; // Slower updates on mobile
Conclusion
The buffering issue on mobile devices is caused by Traefik attempting to buffer an infinite audio stream. By disabling buffering, adding streaming-specific headers, and configuring flush intervals, you can achieve near-instant playback on all devices.
Key takeaway : The issue is not in the frontend code or delay settings — it’s 100% the reverse proxy configuration. With proper Traefik configuration, mobile playback latency drops from 8-10s to 2-3s.