Message Protocol

Understanding Socket.IO events, payloads, and the request/response pattern

Message Protocol

SkySpy uses Socket.IO's event-based protocol for bidirectional communication. Learn about events, payloads, batching, and the request/response pattern.

Event-Based API

Socket.IO uses events instead of raw messages. Each event has:

  • Name (string): The event type (e.g., subscribe, aircraft:update)
  • Payload (usually an object): The data associated with the event
📘

Convention

SkySpy uses colon-separated namespacing for events (e.g., aircraft:update, safety:event). This makes it easy to identify the domain and action.

Client → Server Events

These are the events your client can emit to the server.

EventPayloadDescriptionExample
subscribe{ topics: string[] }Subscribe to topics (e.g., ['aircraft','safety']; use 'all' for all topics){ topics: ['aircraft'] }
unsubscribe{ topics: string[] }Unsubscribe from topics{ topics: ['stats'] }
request{ type, request_id, params? }On-demand query; server replies with response or errorSee request/response section
pingoptional dataCustom keepalive; server replies with pongnull or { timestamp }

Example: Subscribe

// Subscribe to multiple topics
socket.emit('subscribe', { 
  topics: ['aircraft', 'safety', 'alerts'] 
});

// Subscribe to all topics
socket.emit('subscribe', { 
  topics: ['all'] 
});
# Subscribe to multiple topics
sio.emit('subscribe', {
    'topics': ['aircraft', 'safety', 'alerts']
})

# Subscribe to all topics
sio.emit('subscribe', {
    'topics': ['all']
})

Server → Client Events

These are the events the server emits to your client.

Control Events

EventWhenPayloadDescription
subscribedAfter subscribe{ topics, joined?, denied? }Confirms subscription; lists joined and denied topics
unsubscribedAfter unsubscribe{ topics, remaining }Confirms unsubscription; lists remaining subscriptions
responseReply to request{ type, request_id, request_type, data }Successful response to a request event
errorRequest failed or generic error{ type?, request_id?, message }Error message with optional request context
pongReply to ping{ timestamp }Keepalive response

Data Events

EventTopicPayloadDescription
aircraft:snapshotaircraft{ aircraft[], count, timestamp }Initial snapshot on connect or request
aircraft:updateaircraftaircraft list or deltaPeriodic position updates (rate-limited)
aircraft:newaircraftsingle aircraft objectNew aircraft detected in range
aircraft:removeaircraft{ hex, reason? }Aircraft left range or timeout
aircraft:deltaaircraftdelta objectOnly changed fields (bandwidth optimization)
aircraft:heartbeataircraft{ count, timestamp }Periodic keepalive with aircraft count
safety:snapshotsafety{ events[], count, timestamp }Initial safety event snapshot
safety:eventsafetyevent objectNew safety event (TCAS, emergency, etc.)
alert:triggeredalertsalert payloadCustom alert rule fired
acars:messageacarsmessage objectNew ACARS datalink message
stats:updatestatsstats objectLive statistics update
batchall{ messages[], count?, timestamp? }Batched messages (high-frequency optimization)
📘

Payload Compatibility

Payloads match the formats described in the REST API documentation. Only the transport mechanism is different (Socket.IO events vs. HTTP responses).

Batch Messages

To optimize bandwidth, high-frequency updates may be batched into a single batch event.

{
  "messages": [
    {
      "type": "aircraft:update",
      "data": {
        "hex": "A1B2C3",
        "lat": 37.7749,
        "lon": -122.4194,
        "alt_baro": 35000
      }
    },
    {
      "type": "aircraft:update",
      "data": {
        "hex": "D4E5F6",
        "lat": 37.8044,
        "lon": -122.2712,
        "alt_baro": 28000
      }
    }
  ],
  "count": 2,
  "timestamp": "2024-01-15T10:30:00.000Z"
}

Handling Batch Events

socket.on('batch', (data) => {
  data.messages.forEach(msg => {
    // Dispatch to individual handlers
    switch (msg.type) {
      case 'aircraft:update':
        handleAircraftUpdate(msg.data);
        break;
      case 'safety:event':
        handleSafetyEvent(msg.data);
        break;
      default:
        console.log('Unknown message type:', msg.type);
    }
  });
});
@sio.event
def batch(data):
    for msg in data.get('messages', []):
        msg_type = msg.get('type', '')
        msg_data = msg.get('data', msg)
        
        if msg_type == 'aircraft:update':
            handle_aircraft_update(msg_data)
        elif msg_type == 'safety:event':
            handle_safety_event(msg_data)
        else:
            print(f'Unknown message type: {msg_type}')

Critical Events Bypass Batching

Critical event types (alert, safety, emergency) bypass batching and are emitted immediately to ensure low latency for important events.

Batch Configuration

ParameterDefault ValueDescription
Batch Window~200 msTime to collect messages before sending batch
Max Batch Size~50 messagesMaximum messages per batch
Max Batch Bytes~1 MBMaximum payload size per batch
Bypass Typesalert, safety, emergencyEvent types that skip batching

Request/Response Pattern

For on-demand queries (historical data, aircraft info, etc.), use the request event with a unique request_id.

Request Flow

sequenceDiagram
    participant C as Client
    participant S as Server

    C->>S: request { type, request_id, params }

    alt Success
        S->>C: response { request_id, data }
    else Error
        S->>C: error { request_id, message }
    end

Request Format

{
  "type": "aircraft-info",
  "request_id": "req_abc123",
  "params": {
    "icao": "A1B2C3"
  }
}
FieldRequiredDescription
typeYesRequest type (e.g., aircraft-info, sightings, safety-events)
request_idYesUnique identifier to match response (client-generated)
paramsNoParameters specific to the request type

Response Format

Success:

{
  "type": "response",
  "request_id": "req_abc123",
  "request_type": "aircraft-info",
  "data": {
    "icao_hex": "A1B2C3",
    "registration": "N12345",
    "type_code": "B738",
    "operator": "Southwest Airlines",
    "manufactured": "2015",
    "model": "Boeing 737-800"
  }
}

Error:

{
  "type": "error",
  "request_id": "req_abc123",
  "message": "Aircraft not found"
}

Request/Response Helper

Here's a helper function to handle request/response with timeouts.

function request(socket, type, params = {}, timeoutMs = 10000) {
  return new Promise((resolve, reject) => {
    const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2)}`;
    
    const timeout = setTimeout(() => {
      cleanup();
      reject(new Error(`Request timeout: ${type}`));
    }, timeoutMs);

    const onResponse = (data) => {
      if (data.request_id !== requestId) return;
      cleanup();
      resolve(data.data ?? data);
    };
    
    const onError = (data) => {
      if (data.request_id !== requestId) return;
      cleanup();
      reject(new Error(data.message || 'Request failed'));
    };
    
    const cleanup = () => {
      clearTimeout(timeout);
      socket.off('response', onResponse);
      socket.off('error', onError);
    };

    socket.on('response', onResponse);
    socket.on('error', onError);
    socket.emit('request', { type, request_id: requestId, params });
  });
}

// Usage
try {
  const info = await request(socket, 'aircraft-info', { icao: 'A1B2C3' });
  console.log('Aircraft:', info);
} catch (error) {
  console.error('Request failed:', error.message);
}
import uuid
import asyncio
from typing import Any, Dict, Optional

class RequestHelper:
    def __init__(self, sio):
        self.sio = sio
        self.pending = {}
        
        @sio.event
        def response(data):
            request_id = data.get('request_id')
            if request_id in self.pending:
                future = self.pending.pop(request_id)
                future.set_result(data.get('data', data))
        
        @sio.event
        def error(data):
            request_id = data.get('request_id')
            if request_id in self.pending:
                future = self.pending.pop(request_id)
                future.set_exception(Exception(data.get('message', 'Request failed')))
    
    async def request(self, req_type: str, params: Optional[Dict] = None, timeout: float = 10.0) -> Any:
        request_id = f"req_{uuid.uuid4().hex}"
        future = asyncio.Future()
        self.pending[request_id] = future
        
        self.sio.emit('request', {
            'type': req_type,
            'request_id': request_id,
            'params': params or {}
        })
        
        try:
            return await asyncio.wait_for(future, timeout=timeout)
        except asyncio.TimeoutError:
            self.pending.pop(request_id, None)
            raise TimeoutError(f'Request timeout: {req_type}')

# Usage
helper = RequestHelper(sio)
try:
    info = await helper.request('aircraft-info', {'icao': 'A1B2C3'})
    print('Aircraft:', info)
except Exception as e:
    print('Request failed:', e)

Rate Limits

Different topics have different rate limits to optimize bandwidth and prevent overwhelming clients.

Topic / EventMax RateMin IntervalNotes
aircraft:update~10 Hz100 msFull position updates
aircraft:delta~10 Hz100 msDelta updates (changed fields only)
stats:update~0.5 Hz2 sLive statistics
Default~5 Hz200 msOther event types
📘

Client-Side Throttling

If your application can't keep up with the update rate, implement client-side throttling or request lower-frequency updates. Critical events (alerts, safety) are never rate-limited.

Next Steps

📘

Ready to stream data?

Learn about the main namespace to start receiving aircraft positions, safety events, and alerts. Or explore specialized namespaces for audio and mobile features.