Skip to content

Dart WebSockets Complete Guide — Real-Time Communication in Dart

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about Dart WebSockets Complete Guide. We cover key concepts, practical examples, and best practices to help you master this topic.

Dart WebSockets enable real-time bidirectional communication between a client and server over a single persistent TCP connection, using the dart:io WebSocket class for server-side and the web_socket_channel package for cross-platform client usage.

What You Will Learn

  • What WebSockets are and how they differ from HTTP polling
  • Establishing WebSocket connections from Dart clients
  • Sending and receiving messages asynchronously
  • Handling connection drops, reconnection, and heartbeats
  • Building a real-time chat client in Flutter
  • Security best practices for WebSocket communication

Why It Matters

Traditional HTTP follows a request-response pattern: the client asks, the server answers, and the connection closes. For real-time features like live chat, stock tickers, notification feeds, or collaborative editing, this model is inefficient because the client must repeatedly poll the server for new data. WebSockets solve this by keeping a single connection open where either side can send messages at any time. This reduces latency, server load, and bandwidth usage. In Durga Antivirus Pro, WebSockets deliver real-time threat alerts to the dashboard — when a new signature is detected, the server pushes the alert immediately without polling overhead.

Real-World Use

A live sports score app needs to update scores the moment they change. With HTTP polling, the app requests scores every 5 seconds, wasting bandwidth on requests that return no new data 90 percent of the time. With WebSockets, the server sends a message each time a score changes. The app updates instantly, the server uses less CPU, and the user sees real time results.

Learning Path

flowchart LR
  A[Packages] --> B[WebSockets\nYou are here]
  B --> C[GraphQL]
  style B fill:#f90,color:#fff

Understanding WebSocket Protocol

A WebSocket connection starts with an HTTP handshake. The client sends an HTTP Upgrade request, and the server responds with a 101 Switching Protocols status. The connection then upgrades from HTTP to the WebSocket protocol, which uses a lightweight frame-based message format.

The benefits over HTTP polling are significant:

  • Lower latency: Messages arrive instantly instead of waiting for the next poll interval.
  • Reduced overhead: No HTTP headers for each message — WebSocket frames have as little as 2 bytes of overhead.
  • Bidirectional: The server can initiate communication, enabling push notifications without client requests.
  • Persistent connection: One TCP connection handles all messages instead of opening and closing connections repeatedly.

Server-Side WebSocket in Dart

The dart:io library provides a WebSocket class for server-side usage. Here is a minimal echo server:

// echo_server.dart
import 'dart:io';

Future<void> main() async {
  final server = await HttpServer.bind('127.0.0.1', 8080);
  print('WebSocket server running on ws://127.0.0.1:8080');

  await for (final request in server) {
    if (request.uri.path == '/ws') {
      final webSocket = await WebSocketTransformer.upgrade(request);
      print('Client connected');

      webSocket.listen(
        (message) {
          print('Received: $message');
          webSocket.add('Echo: $message');
        },
        onDone: () {
          print('Client disconnected');
        },
        onError: (error) {
          print('Error: $error');
        },
      );
    } else {
      request.response.statusCode = 404;
      request.response.close();
    }
  }
}

Start the server:

dart run echo_server.dart

Output:

WebSocket server running on ws://127.0.0.1:8080

The server listens on port 8080 and upgrades any request to /ws to a WebSocket. Every message received is echoed back with an "Echo:" prefix.

Client-Side WebSocket in Dart

For the client side, the web_socket_channel package provides a cross-platform WebSocket implementation that works on all Dart platforms.

Add the dependency:

dependencies:
  web_socket_channel: ^2.4.0
// ws_client.dart
import 'package:web_socket_channel/web_socket_channel.dart';

void main() {
  final channel = WebSocketChannel.connect(
    Uri.parse('ws://127.0.0.1:8080/ws'),
  );

  channel.stream.listen(
    (message) {
      print('Server says: $message');
    },
    onDone: () {
      print('Connection closed');
    },
    onError: (error) {
      print('Connection error: $error');
    },
  );

  channel.sink.add('Hello from Dart client!');
  channel.sink.add('Is anyone there?');

  // Close after 3 seconds
  Future.delayed(Duration(seconds: 3), () {
    channel.sink.close();
  });
}

Run the client while the server is running:

dart run ws_client.dart

Output:

Server says: Echo: Hello from Dart client!
Server says: Echo: Is anyone there?
Connection closed

Building a Real-Time Chat Application

Let us build a simple chat server and Flutter client that demonstrates real-time messaging.

Chat Server

// chat_server.dart
import 'dart:io';
import 'dart:convert';

final Map<WebSocket, String> clients = {};

Future<void> main() async {
  final server = await HttpServer.bind('0.0.0.0', 8080);
  print('Chat server on ws://0.0.0.0:8080');

  await for (final request in server) {
    if (request.uri.path == '/chat') {
      final ws = await WebSocketTransformer.upgrade(request);
      final username = 'User${clients.length + 1}';
      clients[ws] = username;
      broadcast('${username} joined the chat', ws);
      print('$username connected');

      ws.listen(
        (message) {
          broadcast('${username}: $message', ws);
          print('$username: $message');
        },
        onDone: () {
          broadcast('$username left the chat', null);
          clients.remove(ws);
          print('$username disconnected');
        },
        onError: (error) {
          print('Error: $error');
        },
      );
    } else {
      request.response.statusCode = 404;
      request.response.close();
    }
  }
}

void broadcast(String message, WebSocket? sender) {
  final encoded = jsonEncode({'message': message, 'time': DateTime.now().toIso8601String()});
  for (final client in clients.keys) {
    if (client != sender) {
      client.add(encoded);
    }
  }
}

Flutter Chat Client

// lib/main.dart (Flutter)
import 'package:flutter/material.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'dart:convert';

void main() => runApp(ChatApp());

class ChatApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Dart Chat',
      home: ChatScreen(),
    );
  }
}

class ChatScreen extends StatefulWidget {
  @override
  State<ChatScreen> createState() => _ChatScreenState();
}

class _ChatScreenState extends State<ChatScreen> {
  final channel = WebSocketChannel.connect(Uri.parse('ws://10.0.2.2:8080/chat'));
  final messages = <ChatMessage>[];
  final controller = TextEditingController();

  @override
  void initState() {
    super.initState();
    channel.stream.listen((data) {
      final decoded = jsonDecode(data);
      setState(() {
        messages.add(ChatMessage(decoded['message'], DateTime.parse(decoded['time'])));
      });
    });
  }

  void sendMessage() {
    if (controller.text.isNotEmpty) {
      channel.sink.add(controller.text);
      controller.clear();
    }
  }

  @override
  void dispose() {
    channel.sink.close();
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Chat')),
      body: Column(
        children: [
          Expanded(
            child: ListView.builder(
              itemCount: messages.length,
              itemBuilder: (context, index) {
                return ListTile(title: Text(messages[index].text));
              },
            ),
          ),
          Padding(
            padding: EdgeInsets.all(8.0),
            child: Row(
              children: [
                Expanded(
                  child: TextField(controller: controller, decoration: InputDecoration(labelText: 'Message')),
                ),
                IconButton(icon: Icon(Icons.send), onPressed: sendMessage),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class ChatMessage {
  final String text;
  final DateTime time;
  ChatMessage(this.text, this.time);
}

Handling Disconnections and Reconnection

Production WebSocket clients must handle dropped connections gracefully. Use a reconnection Strategy with exponential backoff:

// reconnecting_client.dart
import 'dart:async';
import 'package:web_socket_channel/web_socket_channel.dart';

class ReconnectingWebSocket {
  WebSocketChannel? _channel;
  final String _url;
  final StreamController<dynamic> _controller = StreamController<dynamic>.broadcast();
  Timer? _reconnectTimer;
  int _attempts = 0;

  ReconnectingWebSocket(this._url);

  Stream<dynamic> get stream => _controller.stream;

  void connect() {
    _channel = WebSocketChannel.connect(Uri.parse(_url));
    _attempts = 0;

    _channel!.stream.listen(
      (data) => _controller.add(data),
      onDone: _scheduleReconnect,
      onError: (_) => _scheduleReconnect,
    );
  }

  void _scheduleReconnect() {
    final delay = Duration(seconds: (_attempts * 2).clamp(1, 60));
    _reconnectTimer = Timer(delay, () {
      _attempts++;
      connect();
    });
  }

  void send(dynamic data) {
    _channel?.sink.add(data);
  }

  void dispose() {
    _reconnectTimer?.cancel();
    _channel?.sink.close();
    _controller.close();
  }
}

The exponential backoff starts at 1 second and doubles with each attempt up to 60 seconds, preventing the client from hammering the server during an outage.

Security Best Practices

WebSocket connections bypass standard HTTP security mechanisms, so extra care is needed:

  • Use wss:// instead of ws://: The secure WebSocket protocol encrypts all traffic, preventing message interception on untrusted networks.
  • Validate origins: On the server side, check the Origin header during the handshake to reject connections from unauthorized domains.
  • Authenticate at connection time: Pass an authentication token as a query parameter (ws://server.com/ws?token=abc123) and validate it before accepting the connection.
  • Rate limit messages: Prevent abuse by limiting the number of messages a client can send per second.
  • Sanitize input: Never trust messages from clients — sanitize all data before broadcasting it to other users.
// Token validation on the server
Future<void> handleRequest(HttpRequest request) async {
  final token = request.uri.queryParameters['token'];
  if (token == null || !isValidToken(token)) {
    request.response.statusCode = 403;
    request.response.write('Forbidden');
    await request.response.close();
    return;
  }
  final ws = await WebSocketTransformer.upgrade(request);
  // Continue with authenticated connection
}

Common Mistakes

  1. Not handling connection closure: WebSocket connections can drop at any time due to network issues, server restarts, or timeouts. Always listen for the onDone event and implement reconnection logic.

  2. Blocking the event loop in message handlers: WebSocket listeners run on the same event loop as the rest of your Dart application. Never use synchronous I/O or long-running computations in a message handler — use Isolate or async processing.

  3. Sending unformatted messages: Establish a consistent message format (JSON with defined fields) to prevent Parsing errors on the receiving end. Both client and server must agree on the message structure.

  4. Ignoring the close code: When closing a WebSocket, the server sends a close code and reason. Standard codes include 1000 (normal closure) and 1001 (going away). Check the code in the onDone handler to distinguish expected from unexpected disconnections.

  5. Using ws:// in production: Unencrypted WebSocket traffic can be intercepted and modified by anyone on the same network. Always use wss:// in production environments.

  6. Broadcasting to disconnected clients: If a client disconnects while a broadcast is in progress, the server throws an error. Check the readyState before sending: if (ws.readyState == WebSocket.open) { ws.add(data); }.

  7. Forgetting to close connections: Dart's garbage collector does not close WebSocket connections automatically. Always call channel.sink.close() when the client no longer needs the connection, otherwise the server accumulates stale connections that waste memory.

Practice Questions

  1. How does a WebSocket connection differ from HTTP polling in terms of latency and overhead?
  2. What is the purpose of the HTTP Upgrade header in the WebSocket handshake?
  3. How would you implement authentication for a WebSocket server?
  4. What is exponential backoff and why is it important for reconnection logic?
  5. Challenge: Build a collaborative whiteboard application where multiple clients can draw on a shared canvas. Use WebSockets to broadcast drawing commands (path, color, stroke width) to all connected clients in real time.

Mini Project

Build a real-time notification system:

  • Server: Accept WebSocket connections at /notifications, authenticate via token, and support channel subscription (e.g., subscribe:user_42). Accept administrative messages to push notifications to specific users or broadcast to all.
  • Client: Flutter app that connects, authenticates, subscribes to user-specific channel, and displays notifications as Material snackbars. Implement automatic reconnection and visual feedback for connection state (connected, reconnecting, disconnected).
  • Test by running the server, connecting two simulated clients, and sending a broadcast notification that both clients receive simultaneously.

FAQ

What is the difference between WebSocket and HTTP/2 Server-Sent Events?

SSE is one-directional (server to client only) and uses standard HTTP. WebSockets are bidirectional and use a custom protocol. If you only need server updates, SSE is simpler. For full-duplex communication like chat or gaming, use WebSockets.

Can I use WebSockets with Flutter Web?

Yes. The web_socket_channel package works on all Flutter platforms including web. On web, it uses the browser's native WebSocket API.

How many concurrent WebSocket connections can a Dart server handle?

A Dart server using dart:io can handle thousands of concurrent connections, since each connection is event-driven and does not consume a thread. The limit is typically network throughput or system file descriptor limits rather than CPU.

How do I test WebSocket code?

Use web_socket_channel for integration tests with a real server. For unit tests, mock the WebSocketChannel by providing a StreamController and verifying that sent messages match expected values.

What happens if a message exceeds the maximum frame size?

The default maximum frame size in Dart is 64 KB. Larger messages cause a WebSocketException. Use streaming (send data in chunks with continuation frames) or increase the limit via the WebSocket constructor's maxMessageSize parameter.

What is Next

Proceed to GraphQL to learn how to query and mutate data using Graphql in Dart. Then build complete applications in the project section starting with Project Todo App.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro