Skip to main content
All articles
· Updated July 3, 2026

How to Send Discord Webhook Messages with Go (Golang)

A practical guide to sending Discord webhook messages in Go using net/http. Send plain text, rich embeds, files, and handle rate limits — no external library needed.

golanggowebhooktutorialdiscord apigo discord 2026net/http

🇷🇺 Also available in Русский

How to Send Discord Webhook Messages with Go (Golang)

Go is a favorite for backend services, CLI tools, and monitoring agents — exactly the kind of software that benefits from posting alerts to Discord. The best part: you can send a Discord webhook in Go using only the standard library, with no external dependencies.

This guide covers plain text, rich embeds, file uploads, custom usernames, and proper rate-limit handling.

What is a Discord Webhook?

A Discord webhook is a URL that posts messages to a channel when it receives an HTTP POST request. Unlike a bot, it needs no gateway connection and no token exchange — perfect for a lightweight Go service. If you don’t have a URL yet, follow our guide on how to get a Discord webhook URL.

Prerequisites

You need Go 1.16 or newer. No go get required — everything below uses net/http, encoding/json, and bytes from the standard library.

Step 1: Send a Simple Text Message

package main

import (
	"bytes"
	"encoding/json"
	"log"
	"net/http"
)

const webhookURL = "https://discord.com/api/webhooks/YOUR_WEBHOOK_URL"

type Payload struct {
	Content string `json:"content,omitempty"`
}

func main() {
	body, err := json.Marshal(Payload{Content: "Hello from Go! 🐹"})
	if err != nil {
		log.Fatal(err)
	}

	resp, err := http.Post(webhookURL, "application/json", bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusNoContent {
		log.Println("Message sent successfully!")
	} else {
		log.Printf("Failed: %s", resp.Status)
	}
}

A successful webhook request returns 204 No Content.

Step 2: Send a Rich Embed

Embeds let you format messages with a title, description, colored bar, and fields. Model them as Go structs so the JSON stays type-safe:

type Embed struct {
	Title       string       `json:"title,omitempty"`
	Description string       `json:"description,omitempty"`
	Color       int          `json:"color,omitempty"`
	Fields      []EmbedField `json:"fields,omitempty"`
	Footer      *EmbedFooter `json:"footer,omitempty"`
	Timestamp   string       `json:"timestamp,omitempty"`
}

type EmbedField struct {
	Name   string `json:"name"`
	Value  string `json:"value"`
	Inline bool   `json:"inline,omitempty"`
}

type EmbedFooter struct {
	Text string `json:"text"`
}

type EmbedPayload struct {
	Embeds []Embed `json:"embeds"`
}

Now build and send it:

func sendEmbed() error {
	embed := Embed{
		Title:       "Server Status Report",
		Description: "All systems are operational.",
		Color:       5763719, // Green in decimal
		Fields: []EmbedField{
			{Name: "CPU Usage", Value: "23%", Inline: true},
			{Name: "Memory", Value: "4.2 GB / 16 GB", Inline: true},
			{Name: "Uptime", Value: "14 days", Inline: false},
		},
		Footer:    &EmbedFooter{Text: "Monitoring Agent"},
		Timestamp: time.Now().Format(time.RFC3339),
	}

	body, _ := json.Marshal(EmbedPayload{Embeds: []Embed{embed}})
	resp, err := http.Post(webhookURL, "application/json", bytes.NewReader(body))
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	return nil
}

Discord colors are decimal integers. A few common ones: red 15548997, green 5763719, blurple 5793266, yellow 16776960.

Step 3: Override Username and Avatar

Add these fields to your payload struct to change how each message appears:

type Payload struct {
	Content   string `json:"content,omitempty"`
	Username  string `json:"username,omitempty"`
	AvatarURL string `json:"avatar_url,omitempty"`
}

body, _ := json.Marshal(Payload{
	Content:   "Deployment complete! ✅",
	Username:  "Deploy Bot",
	AvatarURL: "https://example.com/deploy-avatar.png",
})

Step 4: Upload a File

To attach a file, send multipart/form-data instead of JSON. Put the message payload in a payload_json field alongside the file:

func sendFile(path string) error {
	file, err := os.Open(path)
	if err != nil {
		return err
	}
	defer file.Close()

	var buf bytes.Buffer
	w := multipart.NewWriter(&buf)

	// JSON payload part
	payload, _ := json.Marshal(map[string]string{"content": "Here's the log file:"})
	w.WriteField("payload_json", string(payload))

	// File part
	part, err := w.CreateFormFile("file", filepath.Base(path))
	if err != nil {
		return err
	}
	io.Copy(part, file)
	w.Close()

	req, _ := http.NewRequest("POST", webhookURL, &buf)
	req.Header.Set("Content-Type", w.FormDataContentType())

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	return nil
}

Remember to import bytes, mime/multipart, os, io, and path/filepath. See our dedicated guide on sending files with webhooks for size limits and multi-file uploads.

Step 5: Handle Rate Limits

Discord limits webhooks to roughly 5 requests per 2 seconds. When you exceed it, you get 429 Too Many Requests with a retry_after value. Respect it with a small retry helper:

func sendWithRetry(payload []byte, maxRetries int) error {
	for attempt := 0; attempt < maxRetries; attempt++ {
		resp, err := http.Post(webhookURL, "application/json", bytes.NewReader(payload))
		if err != nil {
			return err
		}

		if resp.StatusCode == http.StatusNoContent {
			resp.Body.Close()
			return nil
		}

		if resp.StatusCode == http.StatusTooManyRequests {
			var r struct {
				RetryAfter float64 `json:"retry_after"`
			}
			json.NewDecoder(resp.Body).Decode(&r)
			resp.Body.Close()
			time.Sleep(time.Duration(r.RetryAfter * float64(time.Second)))
			continue
		}

		resp.Body.Close()
		return fmt.Errorf("unexpected status: %s", resp.Status)
	}
	return fmt.Errorf("all retries exhausted")
}

Our rate limits guide explains the per-channel and per-webhook buckets in detail.

Security: Store the URL in an Environment Variable

Never hardcode the webhook URL in committed source. Read it from the environment:

webhookURL := os.Getenv("DISCORD_WEBHOOK_URL")
if webhookURL == "" {
	log.Fatal("DISCORD_WEBHOOK_URL is not set")
}

A leaked URL lets anyone post to your channel. Review our security best practices for rotation and monitoring.

Next Steps

You now have everything to send Discord messages from any Go program — API servers, cron jobs, or monitoring agents. To design complex layouts visually before coding them, use the free Discord Webhook Builder: build the embed, export the JSON, and drop it into your Go structs.

For automation ideas, see webhook notifications and GitHub Actions integration.