diff --git a/cleanup.go b/cleanup.go
index 822b0b3e7ff007ee03192a7cf96aa15cfb4a4956..82671654352cf03ebf59166afa67a2b7caa7ea1d 100644
--- a/cleanup.go
+++ b/cleanup.go
@@ -6,8 +6,19 @@ import (
 )
 
 func cleanup() {
+	// Close all chatInstance
+	instances := make(map[string]*chatInstance)
+	for key, value := range instancesChannel {
+		instances[key] = value
+	}
+
+	for _, instance := range instances {
+		instance.destroy()
+	}
+
+	// Close session
 	if err := session.Close(); err != nil {
-		log.Fatalf("Error while closing session, %s", err)
+		log.Fatalf("Error closing session, %s", err)
 		os.Exit(1)
 	}
 }
diff --git a/config.go b/config.go
index 40ca7c18048a9c785ae1461bf2d1c98adbb59d37..7494bba2428884e8e00cadb12b6a0b340a8c420b 100644
--- a/config.go
+++ b/config.go
@@ -10,15 +10,17 @@ import (
 var config configPayload
 var configPath string
 var defaultConfig = configPayload{
+	Prefix:    "!",
 	Token:     "TOKEN",
+	Timeout:   120,
 	ChannelID: []string{},
-	Prefix:    "!",
 }
 
 type configPayload struct {
+	Prefix    string
 	Token     string
+	Timeout   int
 	ChannelID []string
-	Prefix    string
 }
 
 func init() {
@@ -45,9 +47,32 @@ func parse() {
 	} else {
 		log.Infof("Loaded config at %s.", configPath)
 	}
+}
 
+func configLate() {
 	allowedChannels = make(map[string]bool)
+	categoryMap = make(map[string]string)
+	guildMap = make(map[string]string)
 	for _, id := range config.ChannelID {
+		if channel, err := session.Channel(id); err != nil {
+			log.Errorf("Error getting channel %s, %s", id, err)
+			continue
+		} else {
+			if channel.ParentID == "" {
+				log.Warnf("Channel %s has no parent.", id)
+				continue
+			} else {
+				if guildMap[channel.GuildID] != "" {
+					log.Warnf("Multiple channels specified for guild %s.", channel.GuildID)
+					continue
+				} else {
+					log.Infof("Channel %s from guild %s with parent %s added.",
+						channel.ID, channel.GuildID, channel.ParentID)
+					guildMap[channel.GuildID] = channel.ID
+					categoryMap[channel.ID] = channel.ParentID
+				}
+			}
+		}
 		allowedChannels[id] = true
 	}
 }
diff --git a/go.mod b/go.mod
index 3c02d0d5c7465bb7aa0475c264fbbbef8e43eb08..087f0bc0b8e5271c89ea3f40d8ffd910b369a73a 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.16
 require (
 	git.randomchars.net/freenitori/embedutil v1.0.2
 	git.randomchars.net/freenitori/log v1.0.0
-	git.randomchars.net/freenitori/multiplexer v1.0.12
+	git.randomchars.net/freenitori/multiplexer v1.0.13
 	github.com/BurntSushi/toml v0.3.1
 	github.com/bwmarrin/discordgo v0.23.2
 	github.com/sirupsen/logrus v1.8.1
diff --git a/go.sum b/go.sum
index ede86a0e4a85a8526464e2d0f1f46174e7eeb9e2..98bc7aee205a6956a8b4cf44f05299e5bc3ff2b9 100644
--- a/go.sum
+++ b/go.sum
@@ -10,6 +10,8 @@ git.randomchars.net/freenitori/log v1.0.0 h1:hU99jGk940I1O5OcaTfnXOpN8ozXiarxhu6
 git.randomchars.net/freenitori/log v1.0.0/go.mod h1:YZFRZgVWDIrbyDGHyDeRlIRWeq0DXamXONxIt12eq2Q=
 git.randomchars.net/freenitori/multiplexer v1.0.12 h1:XsMMSeeaeBtavlsMl7M6ZrW00wa2+zsx/w5gYWR5Qh0=
 git.randomchars.net/freenitori/multiplexer v1.0.12/go.mod h1:Bx9vu2RXDtBrsKBslrhrc8v3IJl5Dna7I/rsHF586w0=
+git.randomchars.net/freenitori/multiplexer v1.0.13 h1:5alUJBGLSrZB2/PE79WTirOCQMCtmnurcUSCOYBr10k=
+git.randomchars.net/freenitori/multiplexer v1.0.13/go.mod h1:Bx9vu2RXDtBrsKBslrhrc8v3IJl5Dna7I/rsHF586w0=
 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/bwmarrin/discordgo v0.23.2 h1:BzrtTktixGHIu9Tt7dEE6diysEF9HWnXeHuoJEt2fH4=
diff --git a/handler.go b/handler.go
index 0efca9b38e8254df83e79a66965f0fec9020a7e1..11e2d22cd56b22089631e41b5f1b82b0e616e56e 100644
--- a/handler.go
+++ b/handler.go
@@ -7,28 +7,39 @@ import (
 )
 
 func init() {
-	m.VoiceStateUpdate = append(m.VoiceStateUpdate, handleVoiceUpdate)
+	m.VoiceStateUpdate = append(m.VoiceStateUpdate, handleChatInitiate)
+	m.VoiceStateUpdate = append(m.VoiceStateUpdate, handleInstanceChannelActivity)
 }
 
 var allowedChannels map[string]bool
 
-func handleVoiceUpdate(context *multiplexer.Context) {
-
-	if context.Channel == nil {
-		return
-	}
-
-	if !allowedChannels[context.Channel.ID] {
+func handleChatInitiate(context *multiplexer.Context) {
+	// Lookup channel in allowed map
+	if context.Channel == nil || !allowedChannels[context.Channel.ID] {
 		return
 	}
 
+	// Get event
 	var event *discordgo.VoiceStateUpdate
 	if e, ok := context.Event.(*discordgo.VoiceStateUpdate); !ok {
 		return
 	} else {
 		event = e
 	}
+	if event.VoiceState != nil {
+		if event.VoiceState.ChannelID == "" {
+			return
+		}
+	} else {
+		return
+	}
 
+	// Lookup already existing channel
+	if instancesUser[event.UserID] != nil {
+		return
+	}
+
+	// Get member from cache and add to cache if not exist
 	var member *discordgo.Member
 	if u, err := context.Session.State.Member(event.GuildID, event.UserID); err != nil {
 		if u, err = context.Session.GuildMember(event.GuildID, event.UserID); err != nil {
@@ -41,16 +52,72 @@ func handleVoiceUpdate(context *multiplexer.Context) {
 	} else {
 		member = u
 	}
-
 	if member == nil {
 		return
 	}
 
-	log.Infof("%s#%s (%s) has joined the designated channel #%s (%s).",
+	// Create new instance
+	instance := newInstance(member)
+	if instance == nil {
+		return
+	}
+
+	log.Infof("%s#%s (%s) has created a new volatile channel #%s (%s) in guild %s (%s).",
 		member.User.Username,
 		member.User.Discriminator,
 		member.User.ID,
-		context.Channel.Name,
-		context.Channel.ID,
+		instance.Channel.Name,
+		instance.Channel.ID,
+		context.Guild.Name,
+		context.Guild.ID,
 		)
+
+	// Move user to newly created volatile channel
+	if err := session.GuildMemberMove(event.GuildID, event.UserID, &instance.Channel.ID); err != nil {
+		log.Errorf("Error moving user %s#%s (%s) to newly created volatile channel #%s (%s), %s",
+			member.User.Username,
+			member.User.Discriminator,
+			member.User.ID,
+			instance.Channel.Name,
+			instance.Channel.ID,
+			err)
+	}
+}
+
+func handleInstanceChannelActivity(context *multiplexer.Context) {
+	// Get event
+	var event *discordgo.VoiceStateUpdate
+	if e, ok := context.Event.(*discordgo.VoiceStateUpdate); !ok {
+		return
+	} else {
+		event = e
+	}
+
+	// Lookup channel in pre-event state
+	if event.BeforeUpdate == nil || instancesChannel[event.BeforeUpdate.ChannelID] == nil {
+		// Add member if join
+		instance := instancesChannel[event.ChannelID]
+		if instance != nil {
+			instance.Members[event.UserID] = true
+		}
+		return
+	}
+
+	// If both before update and current state are same and registered, do not handle
+	if event.BeforeUpdate.ChannelID == event.ChannelID && instancesChannel[event.ChannelID] != nil {
+		return
+	}
+
+	// Remove user from previous channel and destroy if zero length
+	instanceOld := instancesChannel[event.BeforeUpdate.ChannelID]
+	delete(instanceOld.Members, event.UserID)
+	if len(instanceOld.Members) == 0 {
+		instanceOld.destroy()
+	}
+
+	// Record user in new channel if registered
+	instance := instancesChannel[event.ChannelID]
+	if instance != nil {
+		instance.Members[event.UserID] = true
+	}
 }
diff --git a/instance.go b/instance.go
new file mode 100644
index 0000000000000000000000000000000000000000..26485f0387ceee85db739a51fc9f0d31fe56e2b4
--- /dev/null
+++ b/instance.go
@@ -0,0 +1,87 @@
+package main
+
+import (
+	"git.randomchars.net/freenitori/log"
+	"github.com/bwmarrin/discordgo"
+	"time"
+)
+
+var (
+	categoryMap map[string]string
+	guildMap    map[string]string
+	timeout     time.Duration
+)
+
+type chatInstance struct {
+	Initiator      *discordgo.Member
+	Channel        *discordgo.Channel
+	PreviousAction time.Time
+	Members        map[string]bool
+}
+
+var (
+	instancesChannel = make(map[string]*chatInstance)
+	instancesUser    = make(map[string]*chatInstance)
+)
+
+// destroy destroys the chatInstance and removes the corresponding Discord voice channel.
+func (c *chatInstance) destroy() {
+	delete(instancesChannel, c.Channel.ID)
+	delete(instancesUser, c.Initiator.User.ID)
+	if _, err := session.ChannelDelete(c.Channel.ID); err != nil {
+		log.Errorf("Error destroying channel %s initiated by user %s in guild %s, %s",
+			c.Channel.ID, c.Initiator.User.ID, c.Initiator.GuildID, err)
+	}
+	log.Infof("Chat instance with channel %s initiated by user %s#%s (%s) destroyed.",
+		c.Channel.ID, c.Initiator.User.Username, c.Initiator.User.Discriminator, c.Initiator.User.ID)
+}
+
+// actionFree checks if actions are free in chatInstance.
+func (c *chatInstance) actionFree() bool {
+	return c.PreviousAction.Add(timeout).Before(time.Now())
+}
+
+// newInstance creates a new chatInstance on behalf of a discordgo.Member.
+func newInstance(member *discordgo.Member) *chatInstance {
+	instance := setupInstance(member)
+	if instance != nil {
+		instancesChannel[instance.Channel.ID] = instance
+		instancesUser[instance.Initiator.User.ID] = instance
+		return instance
+	}
+	return nil
+}
+
+// setupInstance sets up a new chatInstance on behalf of a discordgo.Member.
+func setupInstance(member *discordgo.Member) *chatInstance {
+	channelID := guildMap[member.GuildID]
+	if channelID == "" {
+		return nil
+	}
+
+	parentID := categoryMap[channelID]
+	if parentID == "" {
+		return nil
+	}
+
+	channel, err := session.GuildChannelCreateComplex(member.GuildID, discordgo.GuildChannelCreateData{
+		Name:      member.User.Username + "#" + member.User.Discriminator,
+		Type:      discordgo.ChannelTypeGuildVoice,
+		Topic:     "Volatile channel created by " + member.User.ID,
+		Bitrate:   64000,
+		UserLimit: 10,
+		ParentID:  parentID,
+	})
+	if err != nil {
+		log.Errorf("Error creating channel on guild %s on behalf of member %s, %s",
+			member.GuildID, member.User.ID, err)
+		return nil
+	}
+
+	return &chatInstance{
+		Initiator:      member,
+		Channel:        channel,
+		PreviousAction: time.Unix(0, 0),
+		Members:        make(map[string]bool),
+	}
+}
diff --git a/main.go b/main.go
index abf5657226ced354a92de82e643c25b34e559692..d3b19c0f322ceff969f55ed78c4630136c159858 100644
--- a/main.go
+++ b/main.go
@@ -51,13 +51,26 @@ func main() {
 	session.UserAgent = "DiscordBot (voice-bot)"
 	session.Token = "Bot " + config.Token
 	session.ShouldReconnectOnError = true
-	session.Identify.Intents = discordgo.IntentsAllWithoutPrivileged
+	session.Identify.Intents = discordgo.IntentsAll
+	session.State.TrackVoice = true
 
 	// Open session
-	if err := session.Open(); err != nil {
-		log.Fatalf("Error while opening session, %s", err)
-		os.Exit(1)
-	}
+	func() {
+		open:
+		if err := session.Open(); err != nil {
+			if session.Identify.Intents == discordgo.IntentsAll {
+				log.Warnf("Wasn't able to start with full intents, some stuff might not work (%s)", err)
+				session.Identify.Intents = discordgo.IntentsAllWithoutPrivileged
+				goto open
+			}
+
+			log.Fatalf("Error while opening session, %s", err)
+			os.Exit(1)
+		}
+	}()
+
+	// Execute late config options
+	configLate()
 
 	// Setup multiplexer
 	m.SessionRegisterHandlers(session)