Skip Navigation
Show nav
Heroku Dev Center Dev Center
  • Get Started
  • Documentation
  • Changelog
  • Search
Heroku Dev Center Dev Center
  • Get Started
    • Node.js
    • Ruby on Rails
    • Ruby
    • Python
    • Java
    • PHP
    • Go
    • Scala
    • Clojure
    • .NET
  • Documentation
  • Changelog
  • More
    Additional Resources
    • Home
    • Elements
    • Products
    • Pricing
    • Careers
    • Help
    • Status
    • Events
    • Podcasts
    • Compliance Center
    Heroku Blog

    Heroku Blog

    Find out what's new with Heroku on our blog.

    Visit Blog
  • Log in or Sign up
View categories

Categories

  • Heroku Architecture
    • Compute (Dynos)
      • Dyno Management
      • Dyno Concepts
      • Dyno Behavior
      • Dyno Reference
      • Dyno Troubleshooting
    • Stacks (operating system images)
    • Networking & DNS
    • Platform Policies
    • Platform Principles
    • Buildpacks
  • Developer Tools
    • AI Tools
    • Command Line
    • Heroku VS Code Extension
  • Deployment
    • Deploying with Git
    • Deploying with Docker
    • Deployment Integrations
  • Continuous Delivery & Integration (Heroku Flow)
    • Continuous Integration
  • Language Support
    • Node.js
      • Working with Node.js
      • Troubleshooting Node.js Apps
      • Node.js Behavior in Heroku
    • Ruby
      • Rails Support
        • Working with Rails
      • Working with Bundler
      • Working with Ruby
      • Ruby Behavior in Heroku
      • Troubleshooting Ruby Apps
    • Python
      • Working with Python
      • Background Jobs in Python
      • Python Behavior in Heroku
      • Working with Django
    • Java
      • Java Behavior in Heroku
      • Working with Java
      • Working with Maven
      • Working with Spring Boot
      • Troubleshooting Java Apps
    • PHP
      • PHP Behavior in Heroku
      • Working with PHP
    • Go
      • Go Dependency Management
    • Scala
    • Clojure
    • .NET
      • Working with .NET
  • Databases & Data Management
    • Heroku Postgres
      • Postgres Basics
      • Postgres Getting Started
      • Postgres Performance
      • Postgres Data Transfer & Preservation
      • Postgres Availability
      • Postgres Special Topics
      • Migrating to Heroku Postgres
    • Heroku Key-Value Store
    • Apache Kafka on Heroku
    • Other Data Stores
  • AI
    • Inference Essentials
    • Inference API
    • Inference Quick Start Guides
    • AI Models
    • Tool Use
    • AI Integrations
    • Vector Database
  • Monitoring & Metrics
    • Logging
  • App Performance
  • Add-ons
    • All Add-ons
  • Collaboration
  • Security
    • App Security
    • Identities & Authentication
      • Single Sign-on (SSO)
    • Private Spaces
      • Infrastructure Networking
    • Compliance
  • Heroku Enterprise
    • Enterprise Accounts
    • Enterprise Teams
  • Patterns & Best Practices
  • Extending Heroku
    • Platform API
    • App Webhooks
    • Heroku Labs
    • Building Add-ons
      • Add-on Development Tasks
      • Add-on APIs
      • Add-on Guidelines & Requirements
    • Building CLI Plugins
    • Developing Buildpacks
    • Dev Center
  • Accounts & Billing
  • Troubleshooting & Support
  • Integrating with Salesforce
    • Heroku AppLink
      • Working with Heroku AppLink
      • Heroku AppLink Reference
      • Getting Started with Heroku AppLink
    • Heroku Connect (Salesforce sync)
      • Heroku Connect Administration
      • Heroku Connect Reference
      • Heroku Connect Troubleshooting
    • Other Salesforce Integrations
  • Language Support
  • Go
  • Using WebSockets on Heroku with Go

Using WebSockets on Heroku with Go

English — 日本語に切り替える

Table of Contents [expand]

  • Prerequisites
  • Create WebSocket app
  • Functionality
  • Back-end
  • Front-end
  • Run locally
  • Deploy
  • Next steps

Last updated March 20, 2026

This tutorial gets you started with a Go application that uses a WebSocket, deployed to Heroku.

Sample code for the demo application is available on GitHub. Edits and enhancements are welcome. Just fork the repository, make your changes and send us a pull request.

Prerequisites

  • Go, Git, and the Heroku CLI (as described in Getting Started with Go).
  • A Heroku user account. Signup is free and instant.

Create WebSocket app

The sample application provides a simple example of using WebSockets with Go. Get the sample app and follow along with the code as you read.

Get the sample app

$ git clone https://github.com/heroku-examples/go-websocket-chat-demo.git
$ cd go-websocket-chat-demo

Functionality

The sample application is a simple chat application that will open a WebSocket to the back-end. Any time a chat message is sent from the browser, it’s sent to the server and then published to a Redis channel. A separate goroutine is subscribed to the same Redis channel and broadcasts the received message to all open WebSocket connections.

There are a few key pieces to this example:

  1. The Gorilla WebSocket library which provides a complete WebSocket implementation.
  2. The go-redis library, which provides a Redis client with support for TLS connections required by Heroku Redis.
  3. Go’s standard library log/slog package for structured logging.
  4. JavaScript on the browser that opens a WebSocket connection to the server and responds to a WebSocket message received from the server.

Let’s take a look at both the back-end and front-end pieces in more detail.

Back-end

With Gorilla’s WebSocket library, we can create WebSocket handlers, much like standard http.Handlers. In this case, we have one endpoint that handles sending and receiving messages named handleWebSocket. A websocket.Upgrader is required to upgrade incoming requests to WebSocket connections. The endpoint is used for both submitting new messages to and receiving messages from the chat service. Incoming messages are received via ws.ReadMessage() as Go byte slices. We take those messages, validate them, and publish them to a Redis channel, so all connected servers can receive updates.

func handleWebSocket(hub *Hub, logger *slog.Logger) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        ws, err := upgrader.Upgrade(w, r, nil)
        if err != nil {
            logger.Error("websocket upgrade failed", "err", err)
            return
        }
        defer ws.Close()

        hub.Register(ws)
        defer hub.Deregister(ws)

        for {
            mt, data, err := ws.ReadMessage()
            if err != nil {
                if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) || err == io.EOF {
                    logger.Info("websocket closed")
                } else {
                    logger.Error("error reading websocket message", "err", err)
                }
                return
            }
            if mt == websocket.TextMessage {
                if _, err := validateMessage(data); err != nil {
                    logger.Error("invalid message", "err", err)
                    continue
                }
                if err := hub.Publish(r.Context(), data); err != nil {
                    logger.Error("failed to publish message", "err", err)
                }
            }
        }
    }
}

Messages received from Redis are broadcast to all WebSocket connections by the Hub‘s Subscribe() method.

func (h *Hub) Subscribe(ctx context.Context) error {
    sub := h.rdb.Subscribe(ctx, channel)
    defer sub.Close()

    h.logger.Info("subscribed to redis channel", "channel", channel)

    ch := sub.Channel()
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        case msg, ok := <-ch:
            if !ok {
                return fmt.Errorf("redis subscription channel closed")
            }
            data := []byte(msg.Payload)
            h.logger.Info("redis message received", "data", msg.Payload)
            if _, err := validateMessage(data); err != nil {
                h.logger.Error("invalid message from redis", "err", err)
                continue
            }
            h.Broadcast(data)
        }
    }
}

Because of this architectural model, you can run this application on as many dynos as you want and all of your users will be able to send and receive updates.

In the main() func, we create a Redis client and a Hub that manages all WebSocket connections and Redis pub/sub. We register the handleWebSocket handler at /ws. The public directory is served by http.FileServer out of public/. We take special care to handle availability of the Redis server, which may not be available on boot and may be restarted as part of maintenance. The app also supports graceful shutdown via OS signals.

func main() {
    logger := slog.Default()

    port := os.Getenv("PORT")
    if port == "" {
        logger.Error("$PORT must be set")
        os.Exit(1)
    }

    redisURL := os.Getenv("REDIS_URL")
    if redisURL == "" {
        logger.Error("$REDIS_URL must be set")
        os.Exit(1)
    }

    rdb, err := newRedisClient(redisURL)
    if err != nil {
        logger.Error("failed to create redis client", "err", err)
        os.Exit(1)
    }

    hub := NewHub(rdb, logger)

    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
    defer stop()

    go func() {
        for {
            if err := waitForRedis(ctx, rdb, logger, func() {
                hub.Broadcast(waitingMsg)
            }); err != nil {
                return
            }
            hub.Broadcast(availableMsg)

            if err := hub.Subscribe(ctx); err != nil {
                if ctx.Err() != nil {
                    return
                }
                logger.Error("redis subscription error, reconnecting", "err", err)
                continue
            }
            return
        }
    }()

    mux := http.NewServeMux()
    mux.Handle("/", http.FileServer(http.Dir("./public")))
    mux.HandleFunc("/ws", handleWebSocket(hub, logger))

    srv := &http.Server{
        Addr:    ":" + port,
        Handler: mux,
    }

    go func() {
        logger.Info("server starting", "port", port)
        if err := srv.ListenAndServe(); err != http.ErrServerClosed {
            logger.Error("server error", "err", err)
            os.Exit(1)
        }
    }()

    <-ctx.Done()
    logger.Info("shutting down")

    shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    srv.Shutdown(shutdownCtx)
    rdb.Close()
}

Front-end

The second part of this is setting up the client side to open the WebSockets connection with the server.

The index page uses Bootstrap for CSS and jQuery. We store all of our static assets in the public folder.

The main WebSocket interaction happens in public/js/application.js, which is loaded by the main page. It opens our WebSocket connection to the server. We use reconnecting-websocket, which automatically reconnects any broken connection in the browser.

With an open WebSocket, the browser receives messages and we define a function to handle this process.

box.onmessage = function(message) {
  var data = JSON.parse(message.data);
  $("#chat-text").append("<div class='panel panel-default'><div class='panel-heading'>" + $('<span/>').text(data.handle).html() + "</div><div class='panel-body'>" + $('<span/>').text(data.text).html() + "</div></div>");
  $("#chat-text").stop().animate({
    scrollTop: $('#chat-text')[0].scrollHeight
  }, 800);
};

The messages we’re using will be a JSON response with two keys: handle (user’s handle) and text (user’s message). When the message is received, it is parsed by JSON and inserted as a new entry in the page.

We override how the submit button works in our input form by using event.preventDefault() to stop the form from actually sending a POST. Instead, we grab the the values from the form and send them as a JSON message over the WebSocket to the server.

$("#input-form").on("submit", function(event) {
  event.preventDefault();
  var handle = $("#input-handle")[0].value;
  var text   = $("#input-text")[0].value;
  box.send(JSON.stringify({ handle: handle, text: text }));
  $("#input-text")[0].value = "";
});

Run locally

The sample application already contains a Procfile, which declares the web process:

web: go-websocket-chat-demo

You can run the app locally using Docker Compose:

$ docker compose up --build

This command starts both the app (on port 5000) and a local Redis instance. Open http://localhost:5000 in your browser to try it out.

Alternatively, if you have Go and Redis installed locally, you can build and run directly:

$ go build -o go-websocket-chat-demo .
$ cp .env.local .env

Modify the .env file as necessary, then launch with the Heroku CLI:

$ heroku local

Deploy

It’s time to deploy your app to Heroku. Create a Heroku app to deploy to:

$ heroku create
Creating app... done, ⬢ radiant-harbor-77012
https://radiant-harbor-77012-9082557b1993.herokuapp.com/ | https://git.heroku.com/radiant-harbor-77012.git

Add a Redis add-on:

$ heroku addons:create heroku-redis
Creating heroku-redis on radiant-harbor-77012... ~$0.004/hour (max $3/month)
Your add-on should be available in a few minutes.
redis-globular-22902 is being created in the background. The app will restart when complete...
Use heroku addons:info redis-globular-22902 to check creation progress
Use heroku addons:docs heroku-redis to view documentation

Deploy your code with git push.

$ git push heroku master
remote: -----> Building on the Heroku-24 stack
remote: -----> Determining which buildpack to use for this app
remote: -----> Go app detected
remote: -----> Detected go modules via go.mod
remote: -----> Detected Module Name: github.com/heroku-examples/go-websocket-chat-demo
remote: -----> Installing go1.25.8
remote: -----> Fetching go1.25.8.linux-amd64.tar.gz
remote: -----> Determining packages to install
remote: -----> Detected the following main packages to install:
remote:        github.com/heroku-examples/go-websocket-chat-demo
remote: -----> Running: go install -v -tags heroku github.com/heroku-examples/go-websocket-chat-demo
remote: -----> Installed the following binaries:
remote:        ./bin/go-websocket-chat-demo
remote: -----> Discovering process types
remote:        Procfile declares types -> web
remote:
remote: -----> Compressing...
remote:        Done: 5.3M
remote: -----> Launching...
remote:        Released v3
remote:        https://radiant-harbor-77012-9082557b1993.herokuapp.com/ deployed to Heroku

Congratulations! Your web app should now be up and running on Heroku.

Next steps

Now that you have deployed your app and understand the basics we’ll want to expand the app to be a bit more robust. This won’t encompass everything you’d want to do before going public, but it will get you along the a path where you’ll be thinking about the right things.

Security

Remember, this is only a demo application and is likely vulnerable to various attacks. Please refer to WebSockets Security for more general guidelines on securing your WebSocket application.

Feedback

Log in to submit feedback.

Information & Support

  • Getting Started
  • Documentation
  • Changelog
  • Compliance Center
  • Training & Education
  • Blog
  • Support Channels
  • Status

Language Reference

  • Node.js
  • Ruby
  • Java
  • PHP
  • Python
  • Go
  • Scala
  • Clojure
  • .NET

Other Resources

  • Careers
  • Elements
  • Products
  • Pricing
  • RSS
    • Dev Center Articles
    • Dev Center Changelog
    • Heroku Blog
    • Heroku News Blog
    • Heroku Engineering Blog
  • Twitter
    • Dev Center Articles
    • Dev Center Changelog
    • Heroku
    • Heroku Status
  • Github
  • LinkedIn
  • © 2026 Salesforce, Inc. All rights reserved. Various trademarks held by their respective owners. Salesforce Tower, 415 Mission Street, 3rd Floor, San Francisco, CA 94105, United States
  • heroku.com
  • Legal
  • Terms of Service
  • Privacy Information
  • Responsible Disclosure
  • Trust
  • Contact
  • Cookie Preferences
  • Your Privacy Choices