diff --git a/internal/db/bundb/account.go b/internal/db/bundb/account.go
index 16c82c08f..f054b1412 100644
--- a/internal/db/bundb/account.go
+++ b/internal/db/bundb/account.go
@@ -36,6 +36,7 @@ import (
 	"github.com/superseriousbusiness/gotosocial/internal/paging"
 	"github.com/superseriousbusiness/gotosocial/internal/state"
 	"github.com/superseriousbusiness/gotosocial/internal/util"
+	"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
 	"github.com/uptrace/bun"
 	"github.com/uptrace/bun/dialect"
 )
@@ -86,7 +87,7 @@ func (a *accountDB) GetAccountsByIDs(ctx context.Context, ids []string) ([]*gtsm
 	// Reorder the statuses by their
 	// IDs to ensure in correct order.
 	getID := func(a *gtsmodel.Account) string { return a.ID }
-	util.OrderBy(accounts, ids, getID)
+	xslices.OrderBy(accounts, ids, getID)
 
 	if gtscontext.Barebones(ctx) {
 		// no need to fully populate.
diff --git a/internal/db/bundb/application.go b/internal/db/bundb/application.go
index fda0ba602..cbba499b0 100644
--- a/internal/db/bundb/application.go
+++ b/internal/db/bundb/application.go
@@ -22,7 +22,7 @@ import (
 
 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 	"github.com/superseriousbusiness/gotosocial/internal/state"
-	"github.com/superseriousbusiness/gotosocial/internal/util"
+	"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
 	"github.com/uptrace/bun"
 )
 
@@ -169,7 +169,7 @@ func (a *applicationDB) GetAllTokens(ctx context.Context) ([]*gtsmodel.Token, er
 	// Reoroder the tokens by their
 	// IDs to ensure in correct order.
 	getID := func(t *gtsmodel.Token) string { return t.ID }
-	util.OrderBy(tokens, tokenIDs, getID)
+	xslices.OrderBy(tokens, tokenIDs, getID)
 
 	return tokens, nil
 }
diff --git a/internal/db/bundb/conversation.go b/internal/db/bundb/conversation.go
index 22ff4fd79..354463111 100644
--- a/internal/db/bundb/conversation.go
+++ b/internal/db/bundb/conversation.go
@@ -31,7 +31,7 @@ import (
 	"github.com/superseriousbusiness/gotosocial/internal/log"
 	"github.com/superseriousbusiness/gotosocial/internal/paging"
 	"github.com/superseriousbusiness/gotosocial/internal/state"
-	"github.com/superseriousbusiness/gotosocial/internal/util"
+	"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
 	"github.com/uptrace/bun"
 	"github.com/uptrace/bun/dialect"
 )
@@ -209,7 +209,7 @@ func (c *conversationDB) getConversationsByLastStatusIDs(
 
 	// Reorder the conversations by their last status IDs to ensure correct order.
 	getID := func(b *gtsmodel.Conversation) string { return b.ID }
-	util.OrderBy(conversations, conversationLastStatusIDs, getID)
+	xslices.OrderBy(conversations, conversationLastStatusIDs, getID)
 
 	if gtscontext.Barebones(ctx) {
 		// no need to fully populate.
@@ -558,7 +558,7 @@ func (c *conversationDB) DeleteStatusFromConversations(ctx context.Context, stat
 
 	// Invalidate cache entries.
 	updatedConversationIDs = append(updatedConversationIDs, deletedConversationIDs...)
-	updatedConversationIDs = util.Deduplicate(updatedConversationIDs)
+	updatedConversationIDs = xslices.Deduplicate(updatedConversationIDs)
 	c.state.Caches.DB.Conversation.InvalidateIDs("ID", updatedConversationIDs)
 
 	return nil
diff --git a/internal/db/bundb/emoji.go b/internal/db/bundb/emoji.go
index db9daf0aa..ee564317e 100644
--- a/internal/db/bundb/emoji.go
+++ b/internal/db/bundb/emoji.go
@@ -31,7 +31,7 @@ import (
 	"github.com/superseriousbusiness/gotosocial/internal/log"
 	"github.com/superseriousbusiness/gotosocial/internal/paging"
 	"github.com/superseriousbusiness/gotosocial/internal/state"
-	"github.com/superseriousbusiness/gotosocial/internal/util"
+	"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
 	"github.com/uptrace/bun"
 	"github.com/uptrace/bun/dialect"
 )
@@ -597,7 +597,7 @@ func (e *emojiDB) GetEmojisByIDs(ctx context.Context, ids []string) ([]*gtsmodel
 	// Reorder the emojis by their
 	// IDs to ensure in correct order.
 	getID := func(e *gtsmodel.Emoji) string { return e.ID }
-	util.OrderBy(emojis, ids, getID)
+	xslices.OrderBy(emojis, ids, getID)
 
 	if gtscontext.Barebones(ctx) {
 		// no need to fully populate.
@@ -661,7 +661,7 @@ func (e *emojiDB) GetEmojiCategoriesByIDs(ctx context.Context, ids []string) ([]
 	// Reorder the categories by their
 	// IDs to ensure in correct order.
 	getID := func(c *gtsmodel.EmojiCategory) string { return c.ID }
-	util.OrderBy(categories, ids, getID)
+	xslices.OrderBy(categories, ids, getID)
 
 	return categories, nil
 }
diff --git a/internal/db/bundb/filter.go b/internal/db/bundb/filter.go
index e68a0bcd0..fe23bb405 100644
--- a/internal/db/bundb/filter.go
+++ b/internal/db/bundb/filter.go
@@ -27,7 +27,7 @@ import (
 	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 	"github.com/superseriousbusiness/gotosocial/internal/state"
-	"github.com/superseriousbusiness/gotosocial/internal/util"
+	"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
 	"github.com/uptrace/bun"
 )
 
@@ -99,7 +99,7 @@ func (f *filterDB) GetFiltersForAccountID(ctx context.Context, accountID string)
 	}
 
 	// Put the filter structs in the same order as the filter IDs.
-	util.OrderBy(filters, filterIDs, func(filter *gtsmodel.Filter) string { return filter.ID })
+	xslices.OrderBy(filters, filterIDs, func(filter *gtsmodel.Filter) string { return filter.ID })
 
 	if gtscontext.Barebones(ctx) {
 		return filters, nil
diff --git a/internal/db/bundb/filterkeyword.go b/internal/db/bundb/filterkeyword.go
index 8a006d10f..0e1d8daeb 100644
--- a/internal/db/bundb/filterkeyword.go
+++ b/internal/db/bundb/filterkeyword.go
@@ -26,7 +26,7 @@ import (
 	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 	"github.com/superseriousbusiness/gotosocial/internal/log"
-	"github.com/superseriousbusiness/gotosocial/internal/util"
+	"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
 	"github.com/uptrace/bun"
 )
 
@@ -140,7 +140,7 @@ func (f *filterDB) getFilterKeywords(ctx context.Context, idColumn string, id st
 	}
 
 	// Put the filter keyword structs in the same order as the filter keyword IDs.
-	util.OrderBy(filterKeywords, filterKeywordIDs, func(filterKeyword *gtsmodel.FilterKeyword) string {
+	xslices.OrderBy(filterKeywords, filterKeywordIDs, func(filterKeyword *gtsmodel.FilterKeyword) string {
 		return filterKeyword.ID
 	})
 
diff --git a/internal/db/bundb/filterstatus.go b/internal/db/bundb/filterstatus.go
index 95919bd2c..1cd924d13 100644
--- a/internal/db/bundb/filterstatus.go
+++ b/internal/db/bundb/filterstatus.go
@@ -25,7 +25,7 @@ import (
 	"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
 	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
-	"github.com/superseriousbusiness/gotosocial/internal/util"
+	"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
 	"github.com/uptrace/bun"
 )
 
@@ -116,7 +116,7 @@ func (f *filterDB) getFilterStatuses(ctx context.Context, idColumn string, id st
 	}
 
 	// Put the filter status structs in the same order as the filter status IDs.
-	util.OrderBy(filterStatuses, filterStatusIDs, func(filterStatus *gtsmodel.FilterStatus) string {
+	xslices.OrderBy(filterStatuses, filterStatusIDs, func(filterStatus *gtsmodel.FilterStatus) string {
 		return filterStatus.ID
 	})
 
diff --git a/internal/db/bundb/interaction.go b/internal/db/bundb/interaction.go
index a04e97905..9fbe00711 100644
--- a/internal/db/bundb/interaction.go
+++ b/internal/db/bundb/interaction.go
@@ -29,7 +29,7 @@ import (
 	"github.com/superseriousbusiness/gotosocial/internal/log"
 	"github.com/superseriousbusiness/gotosocial/internal/paging"
 	"github.com/superseriousbusiness/gotosocial/internal/state"
-	"github.com/superseriousbusiness/gotosocial/internal/util"
+	"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
 	"github.com/uptrace/bun"
 )
 
@@ -113,7 +113,7 @@ func (i *interactionDB) GetInteractionRequestsByIDs(ctx context.Context, ids []s
 	// Reorder the requests by their
 	// IDs to ensure in correct order.
 	getID := func(r *gtsmodel.InteractionRequest) string { return r.ID }
-	util.OrderBy(requests, ids, getID)
+	xslices.OrderBy(requests, ids, getID)
 
 	if gtscontext.Barebones(ctx) {
 		// no need to fully populate.
diff --git a/internal/db/bundb/list.go b/internal/db/bundb/list.go
index 03dff95e3..f81c59c42 100644
--- a/internal/db/bundb/list.go
+++ b/internal/db/bundb/list.go
@@ -31,7 +31,7 @@ import (
 	"github.com/superseriousbusiness/gotosocial/internal/log"
 	"github.com/superseriousbusiness/gotosocial/internal/paging"
 	"github.com/superseriousbusiness/gotosocial/internal/state"
-	"github.com/superseriousbusiness/gotosocial/internal/util"
+	"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
 	"github.com/uptrace/bun"
 )
 
@@ -333,7 +333,7 @@ func (l *listDB) GetListsByIDs(ctx context.Context, ids []string) ([]*gtsmodel.L
 	// Reorder the lists by their
 	// IDs to ensure in correct order.
 	getID := func(l *gtsmodel.List) string { return l.ID }
-	util.OrderBy(lists, ids, getID)
+	xslices.OrderBy(lists, ids, getID)
 
 	if gtscontext.Barebones(ctx) {
 		// no need to fully populate.
@@ -387,12 +387,12 @@ func (l *listDB) PutListEntries(ctx context.Context, entries []*gtsmodel.ListEnt
 	}
 
 	// Collect unique list IDs from the provided list entries.
-	listIDs := util.Collate(entries, func(e *gtsmodel.ListEntry) string {
+	listIDs := xslices.Collate(entries, func(e *gtsmodel.ListEntry) string {
 		return e.ListID
 	})
 
 	// Collect unique follow IDs from the provided list entries.
-	followIDs := util.Collate(entries, func(e *gtsmodel.ListEntry) string {
+	followIDs := xslices.Collate(entries, func(e *gtsmodel.ListEntry) string {
 		return e.FollowID
 	})
 
@@ -441,7 +441,7 @@ func (l *listDB) DeleteAllListEntriesByFollows(ctx context.Context, followIDs ..
 	}
 
 	// Deduplicate IDs before invalidate.
-	listIDs = util.Deduplicate(listIDs)
+	listIDs = xslices.Deduplicate(listIDs)
 
 	// Invalidate all related list entry caches.
 	l.invalidateEntryCaches(ctx, listIDs, followIDs)
diff --git a/internal/db/bundb/media.go b/internal/db/bundb/media.go
index de980a16a..453ad856a 100644
--- a/internal/db/bundb/media.go
+++ b/internal/db/bundb/media.go
@@ -28,7 +28,7 @@ import (
 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 	"github.com/superseriousbusiness/gotosocial/internal/paging"
 	"github.com/superseriousbusiness/gotosocial/internal/state"
-	"github.com/superseriousbusiness/gotosocial/internal/util"
+	"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
 	"github.com/uptrace/bun"
 )
 
@@ -78,7 +78,7 @@ func (m *mediaDB) GetAttachmentsByIDs(ctx context.Context, ids []string) ([]*gts
 	// Reorder the media by their
 	// IDs to ensure in correct order.
 	getID := func(m *gtsmodel.MediaAttachment) string { return m.ID }
-	util.OrderBy(media, ids, getID)
+	xslices.OrderBy(media, ids, getID)
 
 	return media, nil
 }
diff --git a/internal/db/bundb/mention.go b/internal/db/bundb/mention.go
index ba8c0ba11..04aa5d76e 100644
--- a/internal/db/bundb/mention.go
+++ b/internal/db/bundb/mention.go
@@ -28,7 +28,7 @@ import (
 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 	"github.com/superseriousbusiness/gotosocial/internal/log"
 	"github.com/superseriousbusiness/gotosocial/internal/state"
-	"github.com/superseriousbusiness/gotosocial/internal/util"
+	"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
 	"github.com/uptrace/bun"
 )
 
@@ -91,7 +91,7 @@ func (m *mentionDB) GetMentions(ctx context.Context, ids []string) ([]*gtsmodel.
 	// Reorder the mentions by their
 	// IDs to ensure in correct order.
 	getID := func(m *gtsmodel.Mention) string { return m.ID }
-	util.OrderBy(mentions, ids, getID)
+	xslices.OrderBy(mentions, ids, getID)
 
 	if gtscontext.Barebones(ctx) {
 		// no need to fully populate.
diff --git a/internal/db/bundb/notification.go b/internal/db/bundb/notification.go
index 770e84c5c..ef2527637 100644
--- a/internal/db/bundb/notification.go
+++ b/internal/db/bundb/notification.go
@@ -29,7 +29,7 @@ import (
 	"github.com/superseriousbusiness/gotosocial/internal/id"
 	"github.com/superseriousbusiness/gotosocial/internal/log"
 	"github.com/superseriousbusiness/gotosocial/internal/state"
-	"github.com/superseriousbusiness/gotosocial/internal/util"
+	"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
 	"github.com/uptrace/bun"
 )
 
@@ -130,7 +130,7 @@ func (n *notificationDB) GetNotificationsByIDs(ctx context.Context, ids []string
 	// Reorder the notifs by their
 	// IDs to ensure in correct order.
 	getID := func(n *gtsmodel.Notification) string { return n.ID }
-	util.OrderBy(notifs, ids, getID)
+	xslices.OrderBy(notifs, ids, getID)
 
 	if gtscontext.Barebones(ctx) {
 		// no need to fully populate.
diff --git a/internal/db/bundb/poll.go b/internal/db/bundb/poll.go
index f5c33ce9b..b9384774b 100644
--- a/internal/db/bundb/poll.go
+++ b/internal/db/bundb/poll.go
@@ -29,7 +29,7 @@ import (
 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 	"github.com/superseriousbusiness/gotosocial/internal/log"
 	"github.com/superseriousbusiness/gotosocial/internal/state"
-	"github.com/superseriousbusiness/gotosocial/internal/util"
+	"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
 	"github.com/uptrace/bun"
 )
 
@@ -315,7 +315,7 @@ func (p *pollDB) GetPollVotes(ctx context.Context, pollID string) ([]*gtsmodel.P
 	// Reorder the poll votes by their
 	// IDs to ensure in correct order.
 	getID := func(v *gtsmodel.PollVote) string { return v.ID }
-	util.OrderBy(votes, voteIDs, getID)
+	xslices.OrderBy(votes, voteIDs, getID)
 
 	if gtscontext.Barebones(ctx) {
 		// no need to fully populate.
diff --git a/internal/db/bundb/relationship_block.go b/internal/db/bundb/relationship_block.go
index 9738970e5..9578b0e3e 100644
--- a/internal/db/bundb/relationship_block.go
+++ b/internal/db/bundb/relationship_block.go
@@ -27,7 +27,7 @@ import (
 	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 	"github.com/superseriousbusiness/gotosocial/internal/log"
-	"github.com/superseriousbusiness/gotosocial/internal/util"
+	"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
 	"github.com/uptrace/bun"
 )
 
@@ -127,7 +127,7 @@ func (r *relationshipDB) GetBlocksByIDs(ctx context.Context, ids []string) ([]*g
 	// Reorder the blocks by their
 	// IDs to ensure in correct order.
 	getID := func(b *gtsmodel.Block) string { return b.ID }
-	util.OrderBy(blocks, ids, getID)
+	xslices.OrderBy(blocks, ids, getID)
 
 	if gtscontext.Barebones(ctx) {
 		// no need to fully populate.
diff --git a/internal/db/bundb/relationship_follow.go b/internal/db/bundb/relationship_follow.go
index 042d12f37..aea36f39c 100644
--- a/internal/db/bundb/relationship_follow.go
+++ b/internal/db/bundb/relationship_follow.go
@@ -28,7 +28,7 @@ import (
 	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 	"github.com/superseriousbusiness/gotosocial/internal/log"
-	"github.com/superseriousbusiness/gotosocial/internal/util"
+	"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
 	"github.com/uptrace/bun"
 )
 
@@ -103,7 +103,7 @@ func (r *relationshipDB) GetFollowsByIDs(ctx context.Context, ids []string) ([]*
 	// Reorder the follows by their
 	// IDs to ensure in correct order.
 	getID := func(f *gtsmodel.Follow) string { return f.ID }
-	util.OrderBy(follows, ids, getID)
+	xslices.OrderBy(follows, ids, getID)
 
 	if gtscontext.Barebones(ctx) {
 		// no need to fully populate.
@@ -376,7 +376,7 @@ func (r *relationshipDB) DeleteAccountFollows(ctx context.Context, accountID str
 	}
 
 	// Gather the follow IDs that were deleted for removing related list entries.
-	followIDs := util.Gather(nil, deleted, func(follow *gtsmodel.Follow) string {
+	followIDs := xslices.Gather(nil, deleted, func(follow *gtsmodel.Follow) string {
 		return follow.ID
 	})
 
diff --git a/internal/db/bundb/relationship_follow_req.go b/internal/db/bundb/relationship_follow_req.go
index fc0ca5c0a..030c99c58 100644
--- a/internal/db/bundb/relationship_follow_req.go
+++ b/internal/db/bundb/relationship_follow_req.go
@@ -28,7 +28,7 @@ import (
 	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 	"github.com/superseriousbusiness/gotosocial/internal/log"
-	"github.com/superseriousbusiness/gotosocial/internal/util"
+	"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
 	"github.com/uptrace/bun"
 )
 
@@ -103,7 +103,7 @@ func (r *relationshipDB) GetFollowRequestsByIDs(ctx context.Context, ids []strin
 	// Reorder the requests by their
 	// IDs to ensure in correct order.
 	getID := func(f *gtsmodel.FollowRequest) string { return f.ID }
-	util.OrderBy(follows, ids, getID)
+	xslices.OrderBy(follows, ids, getID)
 
 	if gtscontext.Barebones(ctx) {
 		// no need to fully populate.
diff --git a/internal/db/bundb/relationship_mute.go b/internal/db/bundb/relationship_mute.go
index 37d97a64f..b7b7e109e 100644
--- a/internal/db/bundb/relationship_mute.go
+++ b/internal/db/bundb/relationship_mute.go
@@ -28,7 +28,7 @@ import (
 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 	"github.com/superseriousbusiness/gotosocial/internal/log"
 	"github.com/superseriousbusiness/gotosocial/internal/paging"
-	"github.com/superseriousbusiness/gotosocial/internal/util"
+	"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
 	"github.com/uptrace/bun"
 	"github.com/uptrace/bun/dialect"
 )
@@ -109,7 +109,7 @@ func (r *relationshipDB) getMutesByIDs(ctx context.Context, ids []string) ([]*gt
 	// Reorder the mutes by their
 	// IDs to ensure in correct order.
 	getID := func(b *gtsmodel.UserMute) string { return b.ID }
-	util.OrderBy(mutes, ids, getID)
+	xslices.OrderBy(mutes, ids, getID)
 
 	if gtscontext.Barebones(ctx) {
 		// no need to fully populate.
diff --git a/internal/db/bundb/status.go b/internal/db/bundb/status.go
index 5340b63cd..45e9864a3 100644
--- a/internal/db/bundb/status.go
+++ b/internal/db/bundb/status.go
@@ -29,7 +29,7 @@ import (
 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 	"github.com/superseriousbusiness/gotosocial/internal/log"
 	"github.com/superseriousbusiness/gotosocial/internal/state"
-	"github.com/superseriousbusiness/gotosocial/internal/util"
+	"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
 	"github.com/uptrace/bun"
 )
 
@@ -76,7 +76,7 @@ func (s *statusDB) GetStatusesByIDs(ctx context.Context, ids []string) ([]*gtsmo
 	// Reorder the statuses by their
 	// IDs to ensure in correct order.
 	getID := func(s *gtsmodel.Status) string { return s.ID }
-	util.OrderBy(statuses, ids, getID)
+	xslices.OrderBy(statuses, ids, getID)
 
 	if gtscontext.Barebones(ctx) {
 		// no need to fully populate.
diff --git a/internal/db/bundb/statusbookmark.go b/internal/db/bundb/statusbookmark.go
index 1534050da..6cbd7f583 100644
--- a/internal/db/bundb/statusbookmark.go
+++ b/internal/db/bundb/statusbookmark.go
@@ -28,7 +28,7 @@ import (
 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 	"github.com/superseriousbusiness/gotosocial/internal/log"
 	"github.com/superseriousbusiness/gotosocial/internal/state"
-	"github.com/superseriousbusiness/gotosocial/internal/util"
+	"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
 	"github.com/uptrace/bun"
 )
 
@@ -95,7 +95,7 @@ func (s *statusBookmarkDB) GetStatusBookmarksByIDs(ctx context.Context, ids []st
 	// Reorder the bookmarks by their
 	// IDs to ensure in correct order.
 	getID := func(b *gtsmodel.StatusBookmark) string { return b.ID }
-	util.OrderBy(bookmarks, ids, getID)
+	xslices.OrderBy(bookmarks, ids, getID)
 
 	// Populate all loaded bookmarks, removing those we fail
 	// to populate (removes needing so many later nil checks).
diff --git a/internal/db/bundb/statusfave.go b/internal/db/bundb/statusfave.go
index cf20fbba3..c1fa375aa 100644
--- a/internal/db/bundb/statusfave.go
+++ b/internal/db/bundb/statusfave.go
@@ -31,7 +31,7 @@ import (
 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 	"github.com/superseriousbusiness/gotosocial/internal/log"
 	"github.com/superseriousbusiness/gotosocial/internal/state"
-	"github.com/superseriousbusiness/gotosocial/internal/util"
+	"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
 	"github.com/uptrace/bun"
 )
 
@@ -155,7 +155,7 @@ func (s *statusFaveDB) GetStatusFaves(ctx context.Context, statusID string) ([]*
 	// Reorder the statuses by their
 	// IDs to ensure in correct order.
 	getID := func(f *gtsmodel.StatusFave) string { return f.ID }
-	util.OrderBy(faves, faveIDs, getID)
+	xslices.OrderBy(faves, faveIDs, getID)
 
 	if gtscontext.Barebones(ctx) {
 		// no need to fully populate.
@@ -339,7 +339,7 @@ func (s *statusFaveDB) DeleteStatusFaves(ctx context.Context, targetAccountID st
 	}
 
 	// Deduplicate determined status IDs.
-	statusIDs = util.Deduplicate(statusIDs)
+	statusIDs = xslices.Deduplicate(statusIDs)
 
 	// Invalidate any cached status faves for this status ID.
 	s.state.Caches.DB.StatusFave.InvalidateIDs("ID", statusIDs)
diff --git a/internal/db/bundb/tag.go b/internal/db/bundb/tag.go
index 6c3d870f6..dfb80e829 100644
--- a/internal/db/bundb/tag.go
+++ b/internal/db/bundb/tag.go
@@ -28,7 +28,7 @@ import (
 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 	"github.com/superseriousbusiness/gotosocial/internal/paging"
 	"github.com/superseriousbusiness/gotosocial/internal/state"
-	"github.com/superseriousbusiness/gotosocial/internal/util"
+	"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
 	"github.com/uptrace/bun"
 )
 
@@ -102,7 +102,7 @@ func (t *tagDB) GetTags(ctx context.Context, ids []string) ([]*gtsmodel.Tag, err
 	// Reorder the tags by their
 	// IDs to ensure in correct order.
 	getID := func(t *gtsmodel.Tag) string { return t.ID }
-	util.OrderBy(tags, ids, getID)
+	xslices.OrderBy(tags, ids, getID)
 
 	return tags, nil
 }
@@ -301,5 +301,5 @@ func (t *tagDB) GetAccountIDsFollowingTagIDs(ctx context.Context, tagIDs []strin
 
 	// Accounts might be following multiple tags in list,
 	// but we only want to return each account once.
-	return util.Deduplicate(accountIDs), nil
+	return xslices.Deduplicate(accountIDs), nil
 }
diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go
index fdd13f7f0..a953701f8 100644
--- a/internal/federation/federatingprotocol.go
+++ b/internal/federation/federatingprotocol.go
@@ -35,7 +35,7 @@ import (
 	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
 	"github.com/superseriousbusiness/gotosocial/internal/log"
 	"github.com/superseriousbusiness/gotosocial/internal/uris"
-	"github.com/superseriousbusiness/gotosocial/internal/util"
+	"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
 )
 
 type errOtherIRIBlocked struct {
@@ -162,7 +162,7 @@ func (f *Federator) PostInboxRequestBodyHook(ctx context.Context, r *http.Reques
 
 	// OtherIRIs will likely contain some
 	// duplicate entries now, so remove them.
-	otherIRIs = util.DeduplicateFunc(otherIRIs,
+	otherIRIs = xslices.DeduplicateFunc(otherIRIs,
 		(*url.URL).String, // serialized URL is 'key()'
 	)
 
diff --git a/internal/gtsmodel/conversation.go b/internal/gtsmodel/conversation.go
index d17cbe6fe..d3bdcbf1d 100644
--- a/internal/gtsmodel/conversation.go
+++ b/internal/gtsmodel/conversation.go
@@ -22,7 +22,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/superseriousbusiness/gotosocial/internal/util"
+	"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
 )
 
 // Conversation represents direct messages between the owner account and a set of other accounts.
@@ -62,7 +62,7 @@ type Conversation struct {
 
 // ConversationOtherAccountsKey creates an OtherAccountsKey from a list of OtherAccountIDs.
 func ConversationOtherAccountsKey(otherAccountIDs []string) string {
-	otherAccountIDs = util.Deduplicate(otherAccountIDs)
+	otherAccountIDs = xslices.Deduplicate(otherAccountIDs)
 	slices.Sort(otherAccountIDs)
 	return strings.Join(otherAccountIDs, ",")
 }
diff --git a/internal/log/log.go b/internal/log/log.go
index bb2e561b3..52703ef28 100644
--- a/internal/log/log.go
+++ b/internal/log/log.go
@@ -22,11 +22,11 @@ import (
 	"fmt"
 	"log/syslog"
 	"os"
-	"slices"
 	"strings"
 	"time"
 
 	"codeberg.org/gruf/go-kv"
+	"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
 )
 
 var (
@@ -412,7 +412,10 @@ func logf(ctx context.Context, depth int, lvl LEVEL, fields []kv.Field, s string
 	buf.B = append(buf.B, lvlstrs[lvl]...)
 	buf.B = append(buf.B, ' ')
 
-	if ctx != nil {
+	if ctx != nil && len(ctxhooks) > 0 {
+		// Ensure fields have space for hooks (+1 for below).
+		fields = xslices.GrowJust(fields, len(ctxhooks)+1)
+
 		// Pass context through hooks.
 		for _, hook := range ctxhooks {
 			fields = hook(ctx, fields)
@@ -420,9 +423,8 @@ func logf(ctx context.Context, depth int, lvl LEVEL, fields []kv.Field, s string
 	}
 
 	if s != "" {
-		// Append message to log fields.
-		fields = slices.Grow(fields, 1)
-		fields = append(fields, kv.Field{
+		// Append message (if given) as final log field.
+		fields = xslices.AppendJust(fields, kv.Field{
 			K: "msg", V: fmt.Sprintf(s, a...),
 		})
 	}
diff --git a/internal/processing/account/alias.go b/internal/processing/account/alias.go
index a11be0305..d7d4cf547 100644
--- a/internal/processing/account/alias.go
+++ b/internal/processing/account/alias.go
@@ -27,7 +27,7 @@ import (
 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
 	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
-	"github.com/superseriousbusiness/gotosocial/internal/util"
+	"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
 )
 
 func (p *Processor) Alias(
@@ -137,8 +137,8 @@ func (p *Processor) Alias(
 	// Dedupe URIs + accounts, in case someone
 	// provided both an account URL and an
 	// account URI above, for the same account.
-	account.AlsoKnownAsURIs = util.Deduplicate(account.AlsoKnownAsURIs)
-	account.AlsoKnownAs = util.DeduplicateFunc(
+	account.AlsoKnownAsURIs = xslices.Deduplicate(account.AlsoKnownAsURIs)
+	account.AlsoKnownAs = xslices.DeduplicateFunc(
 		account.AlsoKnownAs,
 		func(a *gtsmodel.Account) string {
 			return a.URI
diff --git a/internal/processing/status/create.go b/internal/processing/status/create.go
index 55a78610f..fbc2dadf7 100644
--- a/internal/processing/status/create.go
+++ b/internal/processing/status/create.go
@@ -36,6 +36,7 @@ import (
 	"github.com/superseriousbusiness/gotosocial/internal/typeutils"
 	"github.com/superseriousbusiness/gotosocial/internal/uris"
 	"github.com/superseriousbusiness/gotosocial/internal/util"
+	"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
 )
 
 // Create processes the given form to create a new status, returning the api model representation of that status if it's OK.
@@ -536,9 +537,9 @@ func (p *Processor) processContent(ctx context.Context, parseMention gtsmodel.Pa
 	}
 
 	// Gather all the database IDs from each of the gathered status mentions, tags, and emojis.
-	status.MentionIDs = util.Gather(nil, status.Mentions, func(mention *gtsmodel.Mention) string { return mention.ID })
-	status.TagIDs = util.Gather(nil, status.Tags, func(tag *gtsmodel.Tag) string { return tag.ID })
-	status.EmojiIDs = util.Gather(nil, status.Emojis, func(emoji *gtsmodel.Emoji) string { return emoji.ID })
+	status.MentionIDs = xslices.Gather(nil, status.Mentions, func(mention *gtsmodel.Mention) string { return mention.ID })
+	status.TagIDs = xslices.Gather(nil, status.Tags, func(tag *gtsmodel.Tag) string { return tag.ID })
+	status.EmojiIDs = xslices.Gather(nil, status.Emojis, func(emoji *gtsmodel.Emoji) string { return emoji.ID })
 
 	if status.ContentWarning != "" && len(status.AttachmentIDs) > 0 {
 		// If a content-warning is set, and
diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go
index cfc790bd4..ed8bc1d8d 100644
--- a/internal/typeutils/internaltoas.go
+++ b/internal/typeutils/internaltoas.go
@@ -36,7 +36,7 @@ import (
 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 	"github.com/superseriousbusiness/gotosocial/internal/log"
 	"github.com/superseriousbusiness/gotosocial/internal/uris"
-	"github.com/superseriousbusiness/gotosocial/internal/util"
+	"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
 )
 
 // AccountToAS converts a gts model account into an activity streams person, suitable for federation
@@ -1819,7 +1819,7 @@ func populateValuesForProp[T ap.WithIRI](
 	// Deduplicate the iri strings to
 	// make sure we're not parsing + adding
 	// the same string multiple times.
-	iriStrs = util.Deduplicate(iriStrs)
+	iriStrs = xslices.Deduplicate(iriStrs)
 
 	// Append them to the property.
 	for _, iriStr := range iriStrs {
diff --git a/internal/util/slices.go b/internal/util/xslices/slices.go
similarity index 78%
rename from internal/util/slices.go
rename to internal/util/xslices/slices.go
index 955fe8830..1c1c159b2 100644
--- a/internal/util/slices.go
+++ b/internal/util/xslices/slices.go
@@ -15,12 +15,53 @@
 // You should have received a copy of the GNU Affero General Public License
 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-package util
+package xslices
 
 import (
 	"slices"
 )
 
+// GrowJust increases slice capacity to guarantee
+// extra room 'size', where in the case that it does
+// need to allocate more it ONLY allocates 'size' extra.
+// This is different to typical slices.Grow behaviour,
+// which simply guarantees extra through append() which
+// may allocate more than necessary extra size.
+func GrowJust[T any](in []T, size int) []T {
+
+	if cap(in)-len(in) < size {
+		// Reallocate enough for in + size.
+		in2 := make([]T, len(in), len(in)+size)
+		_ = copy(in2, in)
+		in = in2
+	}
+
+	return in
+}
+
+// AppendJust appends extra elements to slice,
+// ONLY allocating at most len(extra) elements. This
+// is different to the typical append behaviour which
+// will append extra, in a manner to reduce the need
+// for new allocations on every call to append.
+func AppendJust[T any](in []T, extra ...T) []T {
+	l := len(in)
+
+	if cap(in)-l < len(extra) {
+		// Reallocate enough for + extra.
+		in2 := make([]T, l+len(extra))
+		_ = copy(in2, in)
+		in = in2
+	} else {
+		// Reslice for + extra.
+		in = in[:l+len(extra)]
+	}
+
+	// Copy extra into slice.
+	_ = copy(in[l:], extra)
+	return in
+}
+
 // Deduplicate deduplicates entries in the given slice.
 func Deduplicate[T comparable](in []T) []T {
 	var (
diff --git a/internal/util/slices_test.go b/internal/util/xslices/slices_test.go
similarity index 52%
rename from internal/util/slices_test.go
rename to internal/util/xslices/slices_test.go
index c93e489f5..7c62ac77f 100644
--- a/internal/util/slices_test.go
+++ b/internal/util/xslices/slices_test.go
@@ -15,22 +15,90 @@
 // You should have received a copy of the GNU Affero General Public License
 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-package util_test
+package xslices_test
 
 import (
+	"math/rand"
 	"net/url"
 	"slices"
 	"testing"
 
-	"github.com/superseriousbusiness/gotosocial/internal/util"
+	"github.com/stretchr/testify/assert"
+	"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
 )
 
-var (
-	testURLSlice = []*url.URL{}
-)
+func TestGrowJust(t *testing.T) {
+	for _, l := range []int{0, 2, 4, 8, 16, 32, 64} {
+		for _, x := range []int{0, 2, 4, 8, 16, 32, 64} {
+			s := make([]int, l, l+x)
+			for _, g := range []int{0, 2, 4, 8, 16, 32, 64} {
+				s2 := xslices.GrowJust(s, g)
+
+				// Slice length should not be different.
+				assert.Equal(t, len(s), len(s2))
+
+				switch {
+				// If slice already has capacity for
+				// 'g' then it should not be changed.
+				case cap(s) >= len(s)+g:
+					assert.Equal(t, cap(s), cap(s2))
+
+				// Else, returned slice should only
+				// have capacity for original length
+				// plus extra elements, NOTHING MORE.
+				default:
+					assert.Equal(t, cap(s2), len(s)+g)
+				}
+			}
+		}
+	}
+}
+
+func TestAppendJust(t *testing.T) {
+	for _, l := range []int{0, 2, 4, 8, 16, 32, 64} {
+		for _, x := range []int{0, 2, 4, 8, 16, 32, 64} {
+			s := make([]int, l, l+x)
+
+			// Randomize slice.
+			for i := range s {
+				s[i] = rand.Int()
+			}
+
+			for _, a := range []int{0, 2, 4, 8, 16, 32, 64} {
+				toAppend := make([]int, a)
+
+				// Randomize appended vals.
+				for i := range toAppend {
+					toAppend[i] = rand.Int()
+				}
+
+				s2 := xslices.AppendJust(s, toAppend...)
+
+				// Slice length should be as expected.
+				assert.Equal(t, len(s)+a, len(s2))
+
+				// Slice contents should be as expected.
+				assert.Equal(t, append(s, toAppend...), s2)
+
+				switch {
+				// If slice already has capacity for
+				// 'toAppend' then it should not change.
+				case cap(s) >= len(s)+a:
+					assert.Equal(t, cap(s), cap(s2))
+
+				// Else, returned slice should only
+				// have capacity for original length
+				// plus extra elements, NOTHING MORE.
+				default:
+					assert.Equal(t, len(s)+a, cap(s2))
+				}
+			}
+		}
+	}
+}
 
 func TestGather(t *testing.T) {
-	out := util.Gather(nil, []*url.URL{
+	out := xslices.Gather(nil, []*url.URL{
 		{Scheme: "https", Host: "google.com", Path: "/some-search"},
 		{Scheme: "http", Host: "example.com", Path: "/robots.txt"},
 	}, (*url.URL).String)
@@ -41,7 +109,7 @@ func TestGather(t *testing.T) {
 		t.Fatal("unexpected gather output")
 	}
 
-	out = util.Gather([]string{
+	out = xslices.Gather([]string{
 		"starting input string",
 		"another starting input",
 	}, []*url.URL{
@@ -59,7 +127,7 @@ func TestGather(t *testing.T) {
 }
 
 func TestGatherIf(t *testing.T) {
-	out := util.GatherIf(nil, []string{
+	out := xslices.GatherIf(nil, []string{
 		"hello world",
 		"not hello world",
 		"hello world",
@@ -73,7 +141,7 @@ func TestGatherIf(t *testing.T) {
 		t.Fatal("unexpected gatherif output")
 	}
 
-	out = util.GatherIf([]string{
+	out = xslices.GatherIf([]string{
 		"starting input string",
 		"another starting input",
 	}, []string{