Skip to main content

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:
  1. Disable buffering completely
  2. Add streaming-specific headers
  3. 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

2. Streaming Headers

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.
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

1

Update configuration

Add the labels to your docker-compose.yml or create docker-compose.override.yml
2

Restart containers

docker-compose down
docker-compose up -d
3

Verify Traefik applied changes

docker logs traefik | grep azuracast

Test Streaming Performance

# Should respond immediately, not wait
time curl -v https://stream.laurban.cl:8000/media | head -c 1000

# Expected time: < 1 second

Verify Headers

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

Browser DevTools Verification

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?

docker logs traefik 2>&1 | grep -i "buffer\|timeout"
curl -I https://stream.laurban.cl:8000/media | grep -i content-length
Should NOT return a Content-Length header. If present, buffering is still occurring.
# Should respond in < 1 second
time curl -v https://stream.laurban.cl:8000/media | head -c 100
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.

Build docs developers (and LLMs) love