diff --git a/node/icecast.go b/node/icecast.go
index e63c8ccb4d6a6a6a57abad3d6d4065b41e87a822..a9c0bc3092d15fb60731cc357ae4ffbf9d74c091 100644
--- a/node/icecast.go
+++ b/node/icecast.go
@@ -15,6 +15,7 @@ import (
 
 	"git.autistici.org/ale/autoradio"
 	"git.autistici.org/ale/autoradio/instrumentation"
+	"git.autistici.org/ale/autoradio/util"
 )
 
 var (
@@ -67,6 +68,7 @@ func NewIcecastController(publicIp string, maxClients int) *icecastController {
 // Reload the icecast daemon. Redirects output to our standard error
 // for debugging purposes.
 func (ic *icecastController) reload() error {
+	log.Printf("reloading icecast")
 	cmd := exec.Command("/bin/sh", "-c", icecastReloadCmd)
 	cmd.Stdout = os.Stderr
 	cmd.Stderr = os.Stderr
@@ -74,27 +76,28 @@ func (ic *icecastController) reload() error {
 }
 
 // Kill sources connected to local streams.
-func (ic *icecastController) killSources(conf *clusterConfig) error {
-	var anyErr error
+func (ic *icecastController) killSources(conf *clusterConfig) {
 	client := &http.Client{}
 	for _, m := range conf.ListMounts() {
 		req, err := http.NewRequest("GET", fmt.Sprintf("http://localhost:%d/admin/killsource?mount=%s", autoradio.IcecastPort, autoradio.MountNameToIcecastPath(m.Name)), nil)
 		if err != nil {
-			anyErr = err
+			log.Printf("killSources: %v", err)
 			continue
 		}
 		req.SetBasicAuth("admin", getIcecastAdminPassword())
 		resp, err := client.Do(req)
 		if err != nil {
-			anyErr = err
+			log.Printf("killSources: %v", err)
 			continue
 		}
 		resp.Body.Close()
-		if resp.StatusCode != 200 {
-			anyErr = fmt.Errorf("HTTP status %s", resp.Status)
+		// Do not report errors when killing sources on relay
+		// mounts - we only do it in the off chance that the
+		// mount was *not* a relay before.
+		if !m.IsRelay() && resp.StatusCode != 200 {
+			log.Printf("killSources: HTTP status %s", resp.Status)
 		}
 	}
-	return anyErr
 }
 
 // Update reloads the Icecast daemon with a new configuration.
@@ -103,28 +106,36 @@ func (ic *icecastController) Update(conf *clusterConfig, isMaster bool, masterAd
 		return errors.New("unknown system state")
 	}
 
-	// Try to kill sources connected to the local icecast daemon
-	// before reloading, otherwise we'll have problems on master
-	// -> slave transitions (for example, on a restart of radiod)
-	// as sources will be "stuck" preventing the new configuration
-	// from taking effect.
-	if err := ic.killSources(conf); err != nil {
-		log.Printf("error killing sources: %v", err)
-	}
-
 	// Write a new configuration (atomically).
 	ic.config.Update(conf, isMaster, masterAddr.String())
-	tmpf := icecastConfigFile + ".tmp"
-	defer os.Remove(tmpf)
-	if err := ic.config.EncodeToFile(tmpf); err != nil {
+	data, err := ic.config.Encode()
+	if err != nil {
 		return err
 	}
-	if err := os.Rename(tmpf, icecastConfigFile); err != nil {
+
+	changed, err := util.WriteFileIfChanged(icecastConfigFile, data)
+	if err != nil {
 		return err
 	}
+	if changed {
+		// Try to kill sources connected to the local icecast daemon
+		// before reloading, otherwise we'll have problems on master
+		// -> slave transitions (for example, on a restart of radiod)
+		// as sources will be "stuck" preventing the new configuration
+		// from taking effect.
+		//
+		// There is unfortunately a small race condition here, as it
+		// is possible for sources to reconnect before we reload the
+		// icecast daemon.
+		//
+		// TODO: only kill sources whose configuration has changed.
+		ic.killSources(conf)
+
+		// Reload the icecast daemon.
+		return ic.reload()
+	}
 
-	// Tell the Icecast daemon to reload its configuration.
-	return ic.reload()
+	return nil
 }
 
 func (ic *icecastController) GetStatus() *IcecastStatus {
diff --git a/node/icecast_config.go b/node/icecast_config.go
index ffa8dd67517ae2b836df762070b556b0740e7168..be8553632c1ecbe656a3ee34eecc5562aa481a71 100644
--- a/node/icecast_config.go
+++ b/node/icecast_config.go
@@ -9,7 +9,7 @@ import (
 	"log"
 	"net"
 	"net/url"
-	"os"
+	"sort"
 	"strconv"
 
 	"git.autistici.org/ale/autoradio"
@@ -195,22 +195,6 @@ func (c *icecastConfig) Encode() ([]byte, error) {
 	return buf.Bytes(), nil
 }
 
-// EncodeToFile writes the configuration to a file.
-func (c *icecastConfig) EncodeToFile(path string) error {
-	file, err := os.Create(path)
-	if err != nil {
-		return err
-	}
-	defer file.Close()
-
-	data, err := c.Encode()
-	if err != nil {
-		return err
-	}
-	_, err = file.Write(data)
-	return err
-}
-
 func masterMountToIcecastConfig(m *autoradio.Mount) iceMountConfig {
 	mconfig := iceMountConfig{
 		Name:     autoradio.MountNameToIcecastPath(m.Name),
@@ -274,14 +258,26 @@ func slaveMountToIcecastConfig(masterAddr string, m *autoradio.Mount) iceRelayCo
 	}
 }
 
+type mountList []*autoradio.Mount
+
+func (l mountList) Len() int      { return len(l) }
+func (l mountList) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
+func (l mountList) Less(i, j int) bool {
+	return l[i].Name < l[j].Name
+}
+
 // Update the configuration with the current list of mounts and
 // masterelection state. This will clear the Mounts and Relays fields
-// and set them to new values.
+// and set them to new values. The mounts are sorted by name so that
+// the XML representation generated by Encode() is consistent.
 func (ic *icecastConfig) Update(config *clusterConfig, isMaster bool, masterAddr string) {
 	var mounts []iceMountConfig
 	var relays []iceRelayConfig
 
-	for _, m := range config.ListMounts() {
+	allMounts := config.ListMounts()
+	sort.Sort(mountList(allMounts))
+
+	for _, m := range allMounts {
 		switch {
 		case m.IsRelay():
 			if rc, ok := relayToIcecastConfig(m); ok {
diff --git a/node/icecast_config_test.go b/node/icecast_config_test.go
index b3e1f2b2ccfb1fe3cc22a0e14ed731116fd0d706..8794a57b0108053bea43b4e1d00240844ec4fdd1 100644
--- a/node/icecast_config_test.go
+++ b/node/icecast_config_test.go
@@ -1,45 +1,213 @@
 package node
 
 import (
+	"bytes"
+	"fmt"
 	"strings"
 	"testing"
 
 	"git.autistici.org/ale/autoradio"
+	"github.com/aryann/difflib"
 )
 
+const (
+	expectedSlaveConfig = `<!-- Automatically generated, do not edit -->
+
+<icecast>
+  <limits>
+    <clients>1000</clients>
+    <sources>500</sources>
+    <queue-size>1048576</queue-size>
+    <client-timeout>30</client-timeout>
+    <header-timeout>15</header-timeout>
+    <source-timeout>60</source-timeout>
+    <burst-size>131072</burst-size>
+  </limits>
+  <authentication>
+    <source-password>sourcepass</source-password>
+    <admin-user>admin</admin-user>
+    <admin-password>adminpass</admin-password>
+  </authentication>
+  <hostname>1.2.3.4</hostname>
+  <fileserve>1</fileserve>
+  <paths>
+    <basedir>/usr/share/icecast2</basedir>
+    <logdir>/var/log/icecast2</logdir>
+    <webroot>/usr/share/icecast2/web</webroot>
+    <adminroot>/usr/share/icecast2/admin</adminroot>
+  </paths>
+  <logging>
+    <accesslog>access.log</accesslog>
+    <errorlog>error.log</errorlog>
+    <loglevel>3</loglevel>
+    <logsize>10000</logsize>
+  </logging>
+  <security>
+    <chroot>0</chroot>
+  </security>
+  <listen-socket>
+    <bind-address>0.0.0.0</bind-address>
+    <port>8000</port>
+    <shoutcast-compat>0</shoutcast-compat>
+  </listen-socket>
+  <relay>
+    <server>2.3.4.5</server>
+    <port>8000</port>
+    <mount>/_stream/stream1.ogg</mount>
+    <local-mount>/_stream/stream1.ogg</local-mount>
+    <username>user</username>
+    <password>pass</password>
+    <on-demand>1</on-demand>
+    <relay-shoutcast-metadata>1</relay-shoutcast-metadata>
+  </relay>
+  <relay>
+    <server>example.com</server>
+    <port>80</port>
+    <mount>/stream2.ogg</mount>
+    <local-mount>/_stream/stream2.ogg</local-mount>
+    <username></username>
+    <password></password>
+    <on-demand>1</on-demand>
+    <relay-shoutcast-metadata>1</relay-shoutcast-metadata>
+  </relay>
+  <relay>
+    <server>2.3.4.5</server>
+    <port>8000</port>
+    <mount>/_stream/stream3.mp3</mount>
+    <local-mount>/_stream/stream3.mp3</local-mount>
+    <username>user</username>
+    <password>pass</password>
+    <on-demand>1</on-demand>
+    <relay-shoutcast-metadata>1</relay-shoutcast-metadata>
+  </relay>
+</icecast>`
+
+	expectedMasterConfig = `<!-- Automatically generated, do not edit -->
+
+<icecast>
+  <limits>
+    <clients>1000</clients>
+    <sources>500</sources>
+    <queue-size>1048576</queue-size>
+    <client-timeout>30</client-timeout>
+    <header-timeout>15</header-timeout>
+    <source-timeout>60</source-timeout>
+    <burst-size>131072</burst-size>
+  </limits>
+  <authentication>
+    <source-password>sourcepass</source-password>
+    <admin-user>admin</admin-user>
+    <admin-password>adminpass</admin-password>
+  </authentication>
+  <hostname>1.2.3.4</hostname>
+  <fileserve>1</fileserve>
+  <paths>
+    <basedir>/usr/share/icecast2</basedir>
+    <logdir>/var/log/icecast2</logdir>
+    <webroot>/usr/share/icecast2/web</webroot>
+    <adminroot>/usr/share/icecast2/admin</adminroot>
+  </paths>
+  <logging>
+    <accesslog>access.log</accesslog>
+    <errorlog>error.log</errorlog>
+    <loglevel>3</loglevel>
+    <logsize>10000</logsize>
+  </logging>
+  <security>
+    <chroot>0</chroot>
+  </security>
+  <listen-socket>
+    <bind-address>0.0.0.0</bind-address>
+    <port>8000</port>
+    <shoutcast-compat>0</shoutcast-compat>
+  </listen-socket>
+  <relay>
+    <server>example.com</server>
+    <port>80</port>
+    <mount>/stream2.ogg</mount>
+    <local-mount>/_stream/stream2.ogg</local-mount>
+    <username></username>
+    <password></password>
+    <on-demand>1</on-demand>
+    <relay-shoutcast-metadata>1</relay-shoutcast-metadata>
+  </relay>
+  <mount>
+    <mount-name>/_stream/stream1.ogg</mount-name>
+    <username>user</username>
+    <password>pass</password>
+    <fallback-mount>/fallback.ogg</fallback-mount>
+    <fallback-override>1</fallback-override>
+    <hidden>0</hidden>
+  </mount>
+  <mount>
+    <mount-name>/_stream/stream3.mp3</mount-name>
+    <username>user</username>
+    <password>pass</password>
+    <hidden>0</hidden>
+  </mount>
+</icecast>`
+)
+
+// Create an icecastConfig object with known passwords.
+func createTestIcecastConfig() *icecastConfig {
+	ic := newIcecastConfig("1.2.3.4", 1000)
+	ic.Auth.SourcePassword = "sourcepass"
+	ic.Auth.AdminPassword = "adminpass"
+	return ic
+}
+
+func checkStrings(t *testing.T, got, want string) {
+	if got == want {
+		return
+	}
+	var diffout bytes.Buffer
+	for _, d := range difflib.Diff(strings.Split(want, "\n"), strings.Split(got, "\n")) {
+		fmt.Fprintf(&diffout, "%s\n", d.String())
+	}
+	t.Errorf("unexpected result: (+ got, - want)\n\n%s\n", diffout.String())
+}
+
 func TestIcecastConfig(t *testing.T) {
-	mount := &autoradio.Mount{
-		Name:     "/test.ogg",
+	// Create a test config with a few different mount types.
+	c := newClusterConfig()
+	c.setMountIfChanged(&autoradio.Mount{
+		Name:     "/stream1.ogg",
 		Username: "user",
 		Password: "pass",
-	}
-	c := newClusterConfig()
-	c.setMountIfChanged(mount)
+		Fallback: "/fallback.ogg",
+	})
+	c.setMountIfChanged(&autoradio.Mount{
+		Name:     "/stream2.ogg",
+		RelayUrl: "http://example.com/stream2.ogg",
+	})
+	c.setMountIfChanged(&autoradio.Mount{
+		Name:     "/stream3.mp3",
+		Username: "user",
+		Password: "pass",
+		Transcoding: &autoradio.EncodingParams{
+			SourceName: "/stream2.ogg",
+			Format:     "mp3",
+			BitRate:    64,
+			SampleRate: 44100,
+			Channels:   2,
+		},
+	})
 
 	// Test a relay config.
-	ice := newIcecastConfig("1.2.3.4", 1000)
+	ice := createTestIcecastConfig()
 	ice.Update(c, false, "2.3.4.5")
 	output, err := ice.Encode()
 	if err != nil {
 		t.Fatal(err)
 	}
-	outputs := string(output)
-	if !strings.Contains(outputs, "<icecast>") {
-		t.Fatalf("No <icecast> element:\n%s", output)
-	}
-	if !strings.Contains(outputs, "<relay>") {
-		t.Fatalf("Mount not configured as relay:\n%s", output)
-	}
+	checkStrings(t, string(output), expectedSlaveConfig)
 
 	// Test a master config.
-	ice = newIcecastConfig("1.2.3.4", 1000)
+	ice = createTestIcecastConfig()
 	ice.Update(c, true, "2.3.4.5")
 	output, err = ice.Encode()
 	if err != nil {
 		t.Fatal(err)
 	}
-	outputs = string(output)
-	if !strings.Contains(outputs, "<mount>") {
-		t.Fatalf("Mount not configured as master:\n%s", output)
-	}
+	checkStrings(t, string(output), expectedMasterConfig)
 }
diff --git a/node/node.go b/node/node.go
index 72295be705a18ea38090ebb50494f1186eade3fb..152429cd1414e175fec5777e24aad105afda4bbe 100644
--- a/node/node.go
+++ b/node/node.go
@@ -406,11 +406,12 @@ func (rc *RadioNode) updater(stop chan bool) {
 				continue
 			}
 
+			rc.Log.Printf("updating configuration")
+
 			masterAddr := rc.getMasterAddr()
 
 			// Reload the Icecast daemon.
 			icecastReloads.Incr()
-			rc.Log.Printf("reloading icecast config")
 			if err := rc.icecast.Update(rc.config, rc.me.IsMaster(), masterAddr); err != nil {
 				icecastReloadErrors.Incr()
 				rc.Log.Printf("Update(): %v", err)
diff --git a/util/write_file.go b/util/write_file.go
new file mode 100644
index 0000000000000000000000000000000000000000..dee38834d8fde23c059149f997ce5d3c33a23b6c
--- /dev/null
+++ b/util/write_file.go
@@ -0,0 +1,29 @@
+package util
+
+import (
+	"bytes"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+)
+
+// WriteFileIfChanged updates the contents of a file if they have
+// changed. It returns false if the file already exists and its
+// contents are equal to 'data', true in any other case.
+func WriteFileIfChanged(path string, data []byte) (bool, error) {
+	if cur, err := ioutil.ReadFile(path); err == nil && bytes.Equal(cur, data) {
+		return false, nil
+	}
+
+	tmpf, err := ioutil.TempFile(filepath.Dir(path), ".tmp")
+	if err != nil {
+		return true, err
+	}
+	defer os.Remove(tmpf.Name())
+
+	tmpf.Write(data)
+	tmpf.Close()
+	os.Chmod(tmpf.Name(), 0644)
+
+	return true, os.Rename(tmpf.Name(), path)
+}