diff --git a/Cargo.lock b/Cargo.lock
index cc82983..f639d11 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1721,9 +1721,9 @@ dependencies = [
 
 [[package]]
 name = "once_cell"
-version = "1.16.0"
+version = "1.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
+checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
 
 [[package]]
 name = "opaque-debug"
@@ -2937,9 +2937,9 @@ dependencies = [
 
 [[package]]
 name = "toml_edit"
-version = "0.16.2"
+version = "0.17.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd30deba9a1cd7153c22aecf93e86df639e7b81c622b0af8d9255e989991a7b7"
+checksum = "a34cc558345efd7e88b9eda9626df2138b80bb46a7606f695e751c892bc7dac6"
 dependencies = [
  "indexmap",
  "itertools",
@@ -3003,9 +3003,9 @@ dependencies = [
 
 [[package]]
 name = "typed-sled"
-version = "0.2.1"
+version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32b903ab728fcd56542a5c538dfc3d196aeffcdb1dc37e34c49f61782adc5cfa"
+checksum = "1060f05a4450ec5b758da60951b04f225a93a62079316630e76cf25c4034500d"
 dependencies = [
  "bincode",
  "pin-project",
diff --git a/Cargo.toml b/Cargo.toml
index a36724b..db7ae3d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -29,8 +29,8 @@ sled = "0.34.7"
 tera = { version = "1.17.1", features = ["builtins", "date-locale"] }
 tide = { version = "0.16.0", default-features = false, features = ["h1-server", "cookies", "logger"] }
 tokio = { version = "1.23.0", features = ["macros", "rt-multi-thread"] }
-toml_edit = { version = "0.16.2", features = ["easy"] }
-typed-sled = "0.2.1"
+toml_edit = { version = "0.17.1", features = ["easy"] }
+typed-sled = "0.2.3"
 unic-langid = { version = "0.9.1", features = ["macros"] }
 
 [features]
diff --git a/src/db.rs b/src/db.rs
index c4b08f7..8fc1f72 100644
--- a/src/db.rs
+++ b/src/db.rs
@@ -2,6 +2,10 @@ use base64::engine::Engine;
 use serde::{Deserialize, Serialize};
 use sha2::{Digest, Sha256};
 use std::{net::IpAddr, path::Path};
+
+pub use sled::transaction::{
+	ConflictableTransactionError, ConflictableTransactionResult, TransactionError,
+};
 pub use typed_sled::Tree;
 
 const DB_DIR: &str = "db";
diff --git a/src/helpers.rs b/src/helpers.rs
index 8ca1b6a..ee3c369 100644
--- a/src/helpers.rs
+++ b/src/helpers.rs
@@ -3,6 +3,7 @@ use crate::{config::Config, db::*, locales::Locales, queries::*};
 use fluent_bundle::FluentArgs;
 use log::error;
 use std::{net::IpAddr, str::FromStr};
+use typed_sled::transaction::Transactional;
 use unic_langid::LanguageIdentifier;
 
 pub fn new_pending_comment(
@@ -11,16 +12,21 @@ pub fn new_pending_comment(
 	dbs: &Dbs,
 ) -> Result<CommentId, sled::Error> {
 	let comment_id = CommentId::new();
-	dbs.comment
-		.insert(&comment_id, &(comment.clone(), CommentStatus::Pending))?;
-	dbs.comment_pending.insert(
-		&(
-			comment.topic_hash.clone(),
-			comment.post_time,
-			comment_id.clone(),
-		),
-		&(addr, false),
-	)?;
+	(&dbs.comment, &dbs.comment_pending)
+		.transaction(|(db_comment, db_comment_pending)| {
+			db_comment.insert(&comment_id, &(comment.clone(), CommentStatus::Pending))?;
+			db_comment_pending.insert(
+				&(
+					comment.topic_hash.clone(),
+					comment.post_time,
+					comment_id.clone(),
+				),
+				&(addr, false),
+			)?;
+			ConflictableTransactionResult::<_>::Ok(())
+		})
+		.unwrap();
+
 	Ok(comment_id)
 }
 
@@ -40,18 +46,26 @@ pub fn edit_comment(
 			// TODO should we update ip address in comment_pending?
 		}
 		CommentStatus::Approved => {
-			dbs.comment_pending.insert(
-				&(
-					edited_comment.topic_hash.clone(),
-					edited_comment.post_time,
-					comment_id.clone(),
-				),
-				&(addr, true),
-			)?;
-			dbs.comment.insert(
-				&comment_id,
-				&(old_comment, CommentStatus::ApprovedEdited(edited_comment)),
-			)?;
+			(&dbs.comment, &dbs.comment_pending)
+				.transaction(|(db_comment, db_comment_pending)| {
+					db_comment_pending.insert(
+						&(
+							edited_comment.topic_hash.clone(),
+							edited_comment.post_time,
+							comment_id.clone(),
+						),
+						&(addr, true),
+					)?;
+					db_comment.insert(
+						&comment_id,
+						&(
+							old_comment.clone(),
+							CommentStatus::ApprovedEdited(edited_comment.clone()),
+						),
+					)?;
+					ConflictableTransactionResult::<_>::Ok(())
+				})
+				.unwrap();
 		}
 		CommentStatus::ApprovedEdited(_old_edited_comment) => {
 			dbs.comment.insert(
@@ -65,94 +79,108 @@ pub fn edit_comment(
 }
 
 pub fn approve_comment(comment_id: CommentId, dbs: &Dbs) -> Result<(), sled::Error> {
-	if let Some((comment, CommentStatus::Pending)) = dbs.comment.get(&comment_id)? {
-		dbs.comment_pending.remove(&(
-			comment.topic_hash.clone(),
-			comment.post_time,
-			comment_id.clone(),
-		))?;
-		dbs.comment_approved.insert(
-			&(
-				comment.topic_hash.clone(),
-				comment.post_time,
-				comment_id.clone(),
-			),
-			&(),
-		)?;
-		dbs.comment
-			.insert(&comment_id, &(comment, CommentStatus::Approved))?;
-	}
+	(&dbs.comment, &dbs.comment_approved, &dbs.comment_pending)
+		.transaction(|(db_comment, db_comment_approved, db_comment_pending)| {
+			if let Some((comment, CommentStatus::Pending)) = db_comment.get(&comment_id)? {
+				db_comment_pending.remove(&(
+					comment.topic_hash.clone(),
+					comment.post_time,
+					comment_id.clone(),
+				))?;
+				db_comment_approved.insert(
+					&(
+						comment.topic_hash.clone(),
+						comment.post_time,
+						comment_id.clone(),
+					),
+					&(),
+				)?;
+				db_comment.insert(&comment_id, &(comment, CommentStatus::Approved))?;
+			}
+			ConflictableTransactionResult::<_>::Ok(())
+		})
+		.unwrap();
 	Ok(())
 }
 
 pub fn approve_edit(comment_id: CommentId, dbs: &Dbs) -> Result<Option<Comment>, sled::Error> {
-	if let Some((comment, CommentStatus::ApprovedEdited(edited_comment))) =
-		dbs.comment.get(&comment_id)?
-	{
-		dbs.comment_pending.remove(&(
-			edited_comment.topic_hash.clone(),
-			edited_comment.post_time,
-			comment_id.clone(),
-		))?;
-		dbs.comment
-			.insert(&comment_id, &(edited_comment, CommentStatus::Approved))?;
-		return Ok(Some(comment));
-	}
-	Ok(None)
+	Ok((&dbs.comment, &dbs.comment_pending)
+		.transaction(|(db_comment, db_comment_pending)| {
+			if let Some((comment, CommentStatus::ApprovedEdited(edited_comment))) =
+				db_comment.get(&comment_id)?
+			{
+				db_comment_pending.remove(&(
+					edited_comment.topic_hash.clone(),
+					edited_comment.post_time,
+					comment_id.clone(),
+				))?;
+				db_comment.insert(&comment_id, &(edited_comment, CommentStatus::Approved))?;
+				return ConflictableTransactionResult::<_>::Ok(Some(comment));
+			}
+			ConflictableTransactionResult::<_>::Ok(None)
+		})
+		.unwrap())
 }
 
 pub fn remove_comment(
 	comment_id: CommentId,
 	dbs: &Dbs,
 ) -> Result<Option<(Comment, CommentStatus)>, sled::Error> {
-	if let Some((comment, edited_comment)) = dbs.comment.remove(&comment_id)? {
-		match &edited_comment {
-			CommentStatus::Pending => {
-				dbs.comment_pending.remove(&(
-					comment.topic_hash.clone(),
-					comment.post_time,
-					comment_id,
-				))?;
+	Ok((&dbs.comment, &dbs.comment_approved, &dbs.comment_pending)
+		.transaction(|(db_comment, db_comment_approved, db_comment_pending)| {
+			if let Some((comment, edited_comment)) = db_comment.remove(&comment_id)? {
+				match &edited_comment {
+					CommentStatus::Pending => {
+						db_comment_pending.remove(&(
+							comment.topic_hash.clone(),
+							comment.post_time,
+							comment_id.clone(),
+						))?;
+					}
+					CommentStatus::Approved => {
+						db_comment_approved.remove(&(
+							comment.topic_hash.clone(),
+							comment.post_time,
+							comment_id.clone(),
+						))?;
+					}
+					CommentStatus::ApprovedEdited(edited_comment) => {
+						db_comment_pending.remove(&(
+							edited_comment.topic_hash.clone(),
+							edited_comment.post_time,
+							comment_id.clone(),
+						))?;
+						db_comment_approved.remove(&(
+							comment.topic_hash.clone(),
+							comment.post_time,
+							comment_id.clone(),
+						))?;
+					}
+				}
+				return ConflictableTransactionResult::<_>::Ok(Some((comment, edited_comment)));
 			}
-			CommentStatus::Approved => {
-				dbs.comment_approved.remove(&(
-					comment.topic_hash.clone(),
-					comment.post_time,
-					comment_id,
-				))?;
-			}
-			CommentStatus::ApprovedEdited(edited_comment) => {
-				dbs.comment_pending.remove(&(
+			ConflictableTransactionResult::<_>::Ok(None)
+		})
+		.unwrap())
+}
+
+pub fn remove_edit(comment_id: CommentId, dbs: &Dbs) -> Result<Option<Comment>, sled::Error> {
+	Ok((&dbs.comment, &dbs.comment_pending)
+		.transaction(|(db_comment, db_comment_pending)| {
+			if let Some((comment, CommentStatus::ApprovedEdited(edited_comment))) =
+				db_comment.get(&comment_id)?
+			{
+				db_comment_pending.remove(&(
 					edited_comment.topic_hash.clone(),
 					edited_comment.post_time,
 					comment_id.clone(),
 				))?;
-				dbs.comment_approved.remove(&(
-					comment.topic_hash.clone(),
-					comment.post_time,
-					comment_id,
-				))?;
+				db_comment.insert(&comment_id, &(comment.clone(), CommentStatus::Approved))?;
+				return ConflictableTransactionResult::<_>::Ok(Some(comment));
 			}
-		}
-		return Ok(Some((comment, edited_comment)));
-	}
-	Ok(None)
-}
-
-pub fn remove_edit(comment_id: CommentId, dbs: &Dbs) -> Result<Option<Comment>, sled::Error> {
-	if let Some((comment, CommentStatus::ApprovedEdited(edited_comment))) =
-		dbs.comment.get(&comment_id)?
-	{
-		dbs.comment_pending.remove(&(
-			edited_comment.topic_hash.clone(),
-			edited_comment.post_time,
-			comment_id.clone(),
-		))?;
-		dbs.comment
-			.insert(&comment_id, &(comment.clone(), CommentStatus::Approved))?;
-		return Ok(Some(comment));
-	}
-	Ok(None)
+			ConflictableTransactionResult::<_>::Ok(None)
+		})
+		.unwrap())
 }
 
 pub fn iter_comments_by_topic<'a, V: typed_sled::KV>(