Overview
Data channels provide a way to send arbitrary application data between peers using the SCTP protocol over DTLS. Unlike media tracks, data channels can send any binary or text data with configurable reliability and ordering guarantees.
DataChannel Structure
From the Pion source code:
// DataChannel represents a WebRTC DataChannel
// The DataChannel interface represents a network channel
// which can be used for bidirectional peer-to-peer transfers of arbitrary data.
type DataChannel struct {
mu sync . RWMutex
statsID string
label string
ordered bool
maxPacketLifeTime * uint16
maxRetransmits * uint16
protocol string
negotiated bool
id * uint16
readyState atomic . Value // DataChannelState
bufferedAmountLowThreshold uint64
onMessageHandler func ( DataChannelMessage )
onOpenHandler func ()
onCloseHandler func ()
onBufferedAmountLow func ()
onErrorHandler func ( error )
sctpTransport * SCTPTransport
dataChannel * datachannel . DataChannel
api * API
log logging . LeveledLogger
}
Creating Data Channels
Creating on Peer Connection
import " github.com/pion/webrtc/v4 "
// Create data channel with label
dc , err := pc . CreateDataChannel ( "data" , nil )
if err != nil {
panic ( err )
}
// Set up handlers before opening
dc . OnOpen ( func () {
fmt . Println ( "Data channel opened" )
})
dc . OnMessage ( func ( msg webrtc . DataChannelMessage ) {
fmt . Printf ( "Message: %s \n " , string ( msg . Data ))
})
With Configuration
dc , err := pc . CreateDataChannel ( "reliable" , & webrtc . DataChannelInit {
Ordered : true ,
MaxRetransmits : nil , // Unlimited retransmits
MaxPacketLifeTime : nil ,
Protocol : "json" ,
Negotiated : false ,
})
Data channels created with CreateDataChannel() are negotiated in-band. The remote peer receives them via the OnDataChannel callback.
Data Channel Options
Reliability Options
Reliable Ordered All messages delivered in order (default).
Reliable Unordered All messages delivered, order not guaranteed.
Unreliable with Retransmits Limited retransmissions, may lose messages.
Unreliable with Lifetime Messages dropped after time limit.
From the source code, here’s how channel types are determined:
func ( d * DataChannel ) open ( sctpTransport * SCTPTransport ) error {
var channelType datachannel . ChannelType
var reliabilityParameter uint32
switch {
case d . maxPacketLifeTime == nil && d . maxRetransmits == nil :
if d . ordered {
channelType = datachannel . ChannelTypeReliable
} else {
channelType = datachannel . ChannelTypeReliableUnordered
}
case d . maxRetransmits != nil :
reliabilityParameter = uint32 ( * d . maxRetransmits )
if d . ordered {
channelType = datachannel . ChannelTypePartialReliableRexmit
} else {
channelType = datachannel . ChannelTypePartialReliableRexmitUnordered
}
default :
reliabilityParameter = uint32 ( * d . maxPacketLifeTime )
if d . ordered {
channelType = datachannel . ChannelTypePartialReliableTimed
} else {
channelType = datachannel . ChannelTypePartialReliableTimedUnordered
}
}
// Create channel with parameters...
}
Example Configurations
// Reliable, ordered (like TCP)
reliable , _ := pc . CreateDataChannel ( "reliable" , & webrtc . DataChannelInit {
Ordered : true ,
})
// Unreliable, unordered (like UDP)
var maxRetransmits uint16 = 0
unreliable , _ := pc . CreateDataChannel ( "unreliable" , & webrtc . DataChannelInit {
Ordered : false ,
MaxRetransmits : & maxRetransmits ,
})
// Partial reliability with 3 retransmits
var retries uint16 = 3
partial , _ := pc . CreateDataChannel ( "partial" , & webrtc . DataChannelInit {
Ordered : true ,
MaxRetransmits : & retries ,
})
// Time-based reliability (500ms)
var lifetime uint16 = 500
timed , _ := pc . CreateDataChannel ( "timed" , & webrtc . DataChannelInit {
MaxPacketLifeTime : & lifetime ,
})
Sending Data
Text Messages
// SendText sends the text message to the DataChannel peer.
func ( d * DataChannel ) SendText ( s string ) error {
err := d . ensureOpen ()
if err != nil {
return err
}
_ , err = d . dataChannel . WriteDataChannel ([] byte ( s ), true )
return err
}
Usage:
dc . OnOpen ( func () {
if err := dc . SendText ( "Hello, WebRTC!" ); err != nil {
log . Printf ( "Send error: %v " , err )
}
})
Binary Data
// Send sends the binary message to the DataChannel peer.
func ( d * DataChannel ) Send ( data [] byte ) error {
err := d . ensureOpen ()
if err != nil {
return err
}
_ , err = d . dataChannel . WriteDataChannel ( data , false )
return err
}
Usage:
// Send binary data
data := [] byte { 0x 01 , 0x 02 , 0x 03 , 0x 04 }
if err := dc . Send ( data ); err != nil {
log . Printf ( "Send error: %v " , err )
}
// Send JSON
type Message struct {
Type string `json:"type"`
Data string `json:"data"`
}
msg := Message { Type : "chat" , Data : "Hello" }
jsonData , _ := json . Marshal ( msg )
dc . Send ( jsonData )
Always check if the data channel is open before sending. Sending on a closed channel will return an error.
Receiving Data
OnMessage Handler
dc . OnMessage ( func ( msg webrtc . DataChannelMessage ) {
if msg . IsString {
fmt . Printf ( "Text message: %s \n " , string ( msg . Data ))
} else {
fmt . Printf ( "Binary message: %d bytes \n " , len ( msg . Data ))
}
})
The message structure:
type DataChannelMessage struct {
Data [] byte
IsString bool
}
Reading Loop Implementation
From the source code:
func ( d * DataChannel ) readLoop () {
buffer := make ([] byte , sctpMaxMessageSizeUnsetValue )
for {
n , isString , err := d . dataChannel . ReadDataChannel ( buffer )
if err != nil {
if errors . Is ( err , io . ErrShortBuffer ) {
if int64 ( n ) < int64 ( d . api . settingEngine . getSCTPMaxMessageSize ()) {
buffer = append ( buffer , make ([] byte , len ( buffer )) ... )
continue
}
d . log . Errorf (
"Incoming DataChannel message larger then Max Message size %v " ,
d . api . settingEngine . getSCTPMaxMessageSize (),
)
}
d . setReadyState ( DataChannelStateClosed )
if ! errors . Is ( err , io . EOF ) {
d . onError ( err )
}
d . onClose ()
return
}
d . onMessage ( DataChannelMessage {
Data : append ([] byte {}, buffer [: n ] ... ),
IsString : isString ,
})
}
}
Data Channel States
Data channels go through several states:
Connecting
Channel is being established.
Open
Channel is open and ready to send/receive.
Closing
Close has been called, but not complete.
Monitoring State
dc . OnOpen ( func () {
fmt . Printf ( "Data channel ' %s ' opened \n " , dc . Label ())
// Now safe to send
dc . SendText ( "Connected!" )
})
dc . OnClose ( func () {
fmt . Printf ( "Data channel ' %s ' closed \n " , dc . Label ())
})
dc . OnError ( func ( err error ) {
fmt . Printf ( "Data channel error: %v \n " , err )
})
Receiving Data Channels
pc . OnDataChannel ( func ( dc * webrtc . DataChannel ) {
fmt . Printf ( "New data channel: %s \n " , dc . Label ())
dc . OnOpen ( func () {
fmt . Println ( "Data channel opened" )
})
dc . OnMessage ( func ( msg webrtc . DataChannelMessage ) {
// Echo messages back
if msg . IsString {
dc . SendText ( string ( msg . Data ))
} else {
dc . Send ( msg . Data )
}
})
})
From the source:
// OnDataChannel sets an event handler which is invoked when a data
// channel message arrives from a remote peer.
func ( pc * PeerConnection ) OnDataChannel ( f func ( * DataChannel )) {
pc . mu . Lock ()
defer pc . mu . Unlock ()
pc . onDataChannelHandler = f
}
Buffered Amount
Monitor how much data is queued for sending:
// BufferedAmount represents the number of bytes of application data
// that have been queued using send().
func ( d * DataChannel ) BufferedAmount () uint64 {
d . mu . RLock ()
defer d . mu . RUnlock ()
if d . dataChannel == nil {
return 0
}
return d . dataChannel . BufferedAmount ()
}
Usage:
// Set threshold for low buffered amount
dc . SetBufferedAmountLowThreshold ( 1024 * 1024 ) // 1 MB
dc . OnBufferedAmountLow ( func () {
fmt . Println ( "Buffer cleared, safe to send more data" )
})
// Check before sending large data
if dc . BufferedAmount () < 1024 * 1024 {
dc . Send ( largeData )
}
From the source:
func ( d * DataChannel ) SetBufferedAmountLowThreshold ( th uint64 ) {
d . mu . Lock ()
defer d . mu . Unlock ()
d . bufferedAmountLowThreshold = th
if d . dataChannel != nil {
d . dataChannel . SetBufferedAmountLowThreshold ( th )
}
}
Pre-negotiated Data Channels
Create matching channels on both sides without in-band negotiation:
// Peer A
var channelID uint16 = 0
dcA , _ := pc . CreateDataChannel ( "sync" , & webrtc . DataChannelInit {
ID : & channelID ,
Negotiated : true ,
})
// Peer B
var channelID uint16 = 0
dcB , _ := pc . CreateDataChannel ( "sync" , & webrtc . DataChannelInit {
ID : & channelID ,
Negotiated : true ,
})
Pre-negotiated channels don’t require signaling and can be created before the connection is established.
Detached Mode
For advanced use cases, detach the data channel to get raw read/write access:
// Detach allows you to detach the underlying datachannel.
func ( d * DataChannel ) Detach () ( datachannel . ReadWriteCloser , error ) {
d . mu . Lock ()
if ! d . api . settingEngine . detach . DataChannels {
d . mu . Unlock ()
return nil , errDetachNotEnabled
}
if d . dataChannel == nil {
d . mu . Unlock ()
return nil , errDetachBeforeOpened
}
d . detachCalled = true
dataChannel := d . dataChannel
d . mu . Unlock ()
return dataChannel , nil
}
Usage:
import " github.com/pion/webrtc/v4 "
// Enable detach mode
settingEngine := webrtc . SettingEngine {}
settingEngine . DetachDataChannels ()
api := webrtc . NewAPI ( webrtc . WithSettingEngine ( settingEngine ))
pc , _ := api . NewPeerConnection ( config )
dc , _ := pc . CreateDataChannel ( "detached" , nil )
dc . OnOpen ( func () {
// Detach the data channel
raw , err := dc . Detach ()
if err != nil {
panic ( err )
}
// Use as io.ReadWriteCloser
go func () {
buf := make ([] byte , 1024 )
for {
n , err := raw . Read ( buf )
if err != nil {
return
}
// Process raw data
raw . Write ( buf [: n ]) // Echo back
}
}()
})
Closing Data Channels
// Close Closes the DataChannel.
func ( d * DataChannel ) Close () error {
return d . close ( false )
}
// GracefulClose Closes the DataChannel and waits for goroutines to complete.
func ( d * DataChannel ) GracefulClose () error {
return d . close ( true )
}
Usage:
// Normal close
if err := dc . Close (); err != nil {
log . Printf ( "Close error: %v " , err )
}
// Graceful close (waits for read loop)
if err := dc . GracefulClose (); err != nil {
log . Printf ( "Graceful close error: %v " , err )
}
Common Patterns
Request/Response
import " encoding/json "
type Request struct {
ID string `json:"id"`
Method string `json:"method"`
Params interface {} `json:"params"`
}
type Response struct {
ID string `json:"id"`
Result interface {} `json:"result"`
Error * string `json:"error,omitempty"`
}
func sendRequest ( dc * webrtc . DataChannel , method string , params interface {}) {
req := Request {
ID : generateID (),
Method : method ,
Params : params ,
}
data , _ := json . Marshal ( req )
dc . Send ( data )
}
dc . OnMessage ( func ( msg webrtc . DataChannelMessage ) {
var req Request
json . Unmarshal ( msg . Data , & req )
// Handle request and send response
resp := Response {
ID : req . ID ,
Result : handleRequest ( req . Method , req . Params ),
}
data , _ := json . Marshal ( resp )
dc . Send ( data )
})
File Transfer
import " io "
func sendFile ( dc * webrtc . DataChannel , filename string ) error {
file , err := os . Open ( filename )
if err != nil {
return err
}
defer file . Close ()
// Send file info first
info , _ := file . Stat ()
dc . SendText ( fmt . Sprintf ( "FILE: %s : %d " , filename , info . Size ()))
// Send file in chunks
buf := make ([] byte , 16384 ) // 16KB chunks
for {
n , err := file . Read ( buf )
if err == io . EOF {
break
}
if err != nil {
return err
}
// Wait if buffer is full
for dc . BufferedAmount () > 1024 * 1024 {
time . Sleep ( 10 * time . Millisecond )
}
dc . Send ( buf [: n ])
}
dc . SendText ( "EOF" )
return nil
}
Multiplexing Channels
var (
controlChannel * webrtc . DataChannel
dataChannel * webrtc . DataChannel
)
controlChannel , _ = pc . CreateDataChannel ( "control" , nil )
dataChannel , _ = pc . CreateDataChannel ( "data" , & webrtc . DataChannelInit {
Ordered : false ,
})
controlChannel . OnMessage ( func ( msg webrtc . DataChannelMessage ) {
// Handle control messages
handleControl ( string ( msg . Data ))
})
dataChannel . OnMessage ( func ( msg webrtc . DataChannelMessage ) {
// Handle data
processData ( msg . Data )
})
Message Size Keep messages under 16KB for best performance. Larger messages are fragmented.
Buffering Monitor BufferedAmount() to avoid overwhelming the send buffer.
Reliability Use unreliable channels for real-time data where freshness matters more than reliability.
Multiple Channels Create separate channels for different data types to avoid head-of-line blocking.
Next Steps
WebRTC Overview Review core WebRTC concepts
ICE & Connectivity Learn about network connectivity