diff --git a/crypto/vm/boc.cpp b/crypto/vm/boc.cpp
index 11583ede6..bd334cbfc 100644
--- a/crypto/vm/boc.cpp
+++ b/crypto/vm/boc.cpp
@@ -930,7 +930,7 @@ unsigned long long BagOfCells::get_idx_entry_raw(int index) {
  * 
  */
 
-td::Result<Ref<Cell>> std_boc_deserialize(td::Slice data, bool can_be_empty) {
+td::Result<Ref<Cell>> std_boc_deserialize(td::Slice data, bool can_be_empty, bool allow_nonzero_level) {
   if (data.empty() && can_be_empty) {
     return Ref<Cell>();
   }
@@ -946,7 +946,7 @@ td::Result<Ref<Cell>> std_boc_deserialize(td::Slice data, bool can_be_empty) {
   if (root.is_null()) {
     return td::Status::Error("bag of cells has null root cell (?)");
   }
-  if (root->get_level() != 0) {
+  if (!allow_nonzero_level && root->get_level() != 0) {
     return td::Status::Error("bag of cells has a root with non-zero level");
   }
   return std::move(root);
diff --git a/crypto/vm/boc.h b/crypto/vm/boc.h
index c7a1810d7..1bff5b257 100644
--- a/crypto/vm/boc.h
+++ b/crypto/vm/boc.h
@@ -323,7 +323,7 @@ class BagOfCells {
                                                      std::vector<td::uint8>* cell_should_cache);
 };
 
-td::Result<Ref<Cell>> std_boc_deserialize(td::Slice data, bool can_be_empty = false);
+td::Result<Ref<Cell>> std_boc_deserialize(td::Slice data, bool can_be_empty = false, bool allow_nonzero_level = false);
 td::Result<td::BufferSlice> std_boc_serialize(Ref<Cell> root, int mode = 0);
 
 td::Result<std::vector<Ref<Cell>>> std_boc_deserialize_multi(td::Slice data,
diff --git a/crypto/vm/db/CellStorage.cpp b/crypto/vm/db/CellStorage.cpp
index acc55898a..303d46503 100644
--- a/crypto/vm/db/CellStorage.cpp
+++ b/crypto/vm/db/CellStorage.cpp
@@ -98,7 +98,7 @@ class RefcntCellParser {
       auto size = parser.get_left_len();
       td::Slice data = parser.template fetch_string_raw<td::Slice>(size);
       if (stored_boc_) {
-        TRY_RESULT(boc, vm::std_boc_deserialize(data));
+        TRY_RESULT(boc, vm::std_boc_deserialize(data, false, true));
         TRY_RESULT(loaded_cell, boc->load_cell());
         cell = std::move(loaded_cell.data_cell);
         return td::Status::OK();
diff --git a/validator-engine/validator-engine.cpp b/validator-engine/validator-engine.cpp
index d98c296c9..1d9223f2b 100644
--- a/validator-engine/validator-engine.cpp
+++ b/validator-engine/validator-engine.cpp
@@ -3775,11 +3775,15 @@ int main(int argc, char *argv[]) {
     acts.push_back([&x, at]() { td::actor::send_closure(x, &ValidatorEngine::schedule_shutdown, (double)at); });
     return td::Status::OK();
   });
-  p.add_checked_option('\0', "celldb-compress-depth", "(default: 0)", [&](td::Slice arg) {
-    TRY_RESULT(value, td::to_integer_safe<td::uint32>(arg));
-    acts.push_back([&x, value]() { td::actor::send_closure(x, &ValidatorEngine::set_celldb_compress_depth, value); });
-    return td::Status::OK();
-  });
+  p.add_checked_option('\0', "celldb-compress-depth",
+                       "optimize celldb by storing cells of depth X with whole subtrees (experimental, default: 0)",
+                       [&](td::Slice arg) {
+                         TRY_RESULT(value, td::to_integer_safe<td::uint32>(arg));
+                         acts.push_back([&x, value]() {
+                           td::actor::send_closure(x, &ValidatorEngine::set_celldb_compress_depth, value);
+                         });
+                         return td::Status::OK();
+                       });
   auto S = p.run(argc, argv);
   if (S.is_error()) {
     LOG(ERROR) << "failed to parse options: " << S.move_as_error();
diff --git a/validator/db/celldb.cpp b/validator/db/celldb.cpp
index 6a2b46992..d29126cea 100644
--- a/validator/db/celldb.cpp
+++ b/validator/db/celldb.cpp
@@ -23,6 +23,7 @@
 
 #include "ton/ton-tl.hpp"
 #include "ton/ton-io.hpp"
+#include "common/delay.h"
 
 namespace ton {
 
@@ -68,14 +69,16 @@ CellDbIn::CellDbIn(td::actor::ActorId<RootDb> root_db, td::actor::ActorId<CellDb
 }
 
 void CellDbIn::start_up() {
-  on_load_callback_ = [db = actor_id(this),
+  on_load_callback_ = [actor = std::make_shared<td::actor::ActorOwn<MigrationProxy>>(
+                           td::actor::create_actor<MigrationProxy>("celldbmigration", actor_id(this))),
                        compress_depth = opts_->get_celldb_compress_depth()](const vm::CellLoader::LoadResult& res) {
     if (res.cell_.is_null()) {
       return;
     }
     bool expected_stored_boc = res.cell_->get_depth() == compress_depth && compress_depth != 0;
     if (expected_stored_boc != res.stored_boc_) {
-      td::actor::send_closure(db, &CellDbIn::migrate_cell, td::Bits256{res.cell_->get_hash().bits()});
+      td::actor::send_closure(*actor, &CellDbIn::MigrationProxy::migrate_cell,
+                              td::Bits256{res.cell_->get_hash().bits()});
     }
   };
 
@@ -156,6 +159,13 @@ void CellDbIn::alarm() {
   if (migrate_after_ && migrate_after_.is_in_past()) {
     migrate_cells();
   }
+  if (migration_stats_ && migration_stats_->end_at_.is_in_past()) {
+    LOG(INFO) << "CellDb migration, " << migration_stats_->start_.elapsed()
+              << "s stats: batches=" << migration_stats_->batches_ << " migrated=" << migration_stats_->migrated_cells_
+              << " checked=" << migration_stats_->checked_cells_ << " time=" << migration_stats_->total_time_
+              << " queue_size=" << cells_to_migrate_.size();
+    migration_stats_ = {};
+  }
   auto E = get_block(get_empty_key_hash()).move_as_ok();
   auto N = get_block(E.next).move_as_ok();
   if (N.is_empty()) {
@@ -291,23 +301,31 @@ void CellDbIn::set_block(KeyHash key_hash, DbEntry e) {
 
 void CellDbIn::migrate_cell(td::Bits256 hash) {
   cells_to_migrate_.insert(hash);
-  if (cells_to_migrate_.size() >= 32) {
-    migrate_cells();
-  } else if (!migrate_after_) {
-    migrate_after_ = td::Timestamp::in(1.0);
+  if (!migration_active_) {
+    migration_active_ = true;
+    migrate_after_ = td::Timestamp::in(10.0);
   }
 }
 
 void CellDbIn::migrate_cells() {
+  migrate_after_ = td::Timestamp::never();
   if (cells_to_migrate_.empty()) {
+    migration_active_ = false;
     return;
   }
+  td::Timer timer;
+  if (!migration_stats_) {
+    migration_stats_ = std::make_unique<MigrationStats>();
+  }
   vm::CellStorer stor{*cell_db_};
   auto loader = std::make_unique<vm::CellLoader>(cell_db_->snapshot());
   boc_->set_loader(std::make_unique<vm::CellLoader>(*loader)).ensure();
   cell_db_->begin_write_batch().ensure();
-  td::uint32 cnt = 0;
-  for (const auto& hash : cells_to_migrate_) {
+  td::uint32 checked = 0, migrated = 0;
+  for (auto it = cells_to_migrate_.begin(); it != cells_to_migrate_.end() && checked < 128; ) {
+    ++checked;
+    td::Bits256 hash = *it;
+    it = cells_to_migrate_.erase(it);
     auto R = loader->load(hash.as_slice(), true, boc_->as_ext_cell_creator());
     if (R.is_error()) {
       continue;
@@ -318,18 +336,27 @@ void CellDbIn::migrate_cells() {
     bool expected_stored_boc =
         R.ok().cell_->get_depth() == opts_->get_celldb_compress_depth() && opts_->get_celldb_compress_depth() != 0;
     if (expected_stored_boc != R.ok().stored_boc_) {
-      ++cnt;
+      ++migrated;
       stor.set(R.ok().refcnt(), R.ok().cell_, expected_stored_boc).ensure();
     }
   }
-  cells_to_migrate_.clear();
-  if (cnt > 0) {
-    LOG(DEBUG) << "Migrated " << cnt << " cells";
-  }
   cell_db_->commit_write_batch().ensure();
   boc_->set_loader(std::make_unique<vm::CellLoader>(cell_db_->snapshot(), on_load_callback_)).ensure();
   td::actor::send_closure(parent_, &CellDb::update_snapshot, cell_db_->snapshot());
-  migrate_after_ = td::Timestamp::never();
+
+  double time = timer.elapsed();
+  LOG(DEBUG) << "CellDb migration: migrated=" << migrated << " checked=" << checked << " time=" << time;
+  ++migration_stats_->batches_;
+  migration_stats_->migrated_cells_ += migrated;
+  migration_stats_->checked_cells_ += checked;
+  migration_stats_->total_time_ += time;
+
+  if (cells_to_migrate_.empty()) {
+    migration_active_ = false;
+  } else {
+    delay_action([SelfId = actor_id(this)] { td::actor::send_closure(SelfId, &CellDbIn::migrate_cells); },
+                 td::Timestamp::in(time * 2));
+  }
 }
 
 void CellDb::load_cell(RootHash hash, td::Promise<td::Ref<vm::DataCell>> promise) {
@@ -361,14 +388,16 @@ void CellDb::start_up() {
   boc_ = vm::DynamicBagOfCellsDb::create();
   boc_->set_celldb_compress_depth(opts_->get_celldb_compress_depth());
   cell_db_ = td::actor::create_actor<CellDbIn>("celldbin", root_db_, actor_id(this), path_, opts_);
-  on_load_callback_ = [db = cell_db_.get(),
+  on_load_callback_ = [actor = std::make_shared<td::actor::ActorOwn<CellDbIn::MigrationProxy>>(
+                           td::actor::create_actor<CellDbIn::MigrationProxy>("celldbmigration", cell_db_.get())),
                        compress_depth = opts_->get_celldb_compress_depth()](const vm::CellLoader::LoadResult& res) {
     if (res.cell_.is_null()) {
       return;
     }
     bool expected_stored_boc = res.cell_->get_depth() == compress_depth && compress_depth != 0;
     if (expected_stored_boc != res.stored_boc_) {
-      td::actor::send_closure(db, &CellDbIn::migrate_cell, td::Bits256{res.cell_->get_hash().bits()});
+      td::actor::send_closure(*actor, &CellDbIn::MigrationProxy::migrate_cell,
+                              td::Bits256{res.cell_->get_hash().bits()});
     }
   };
 }
diff --git a/validator/db/celldb.hpp b/validator/db/celldb.hpp
index 6545d5970..a2a84ab4a 100644
--- a/validator/db/celldb.hpp
+++ b/validator/db/celldb.hpp
@@ -107,6 +107,30 @@ class CellDbIn : public CellDbBase {
   std::function<void(const vm::CellLoader::LoadResult&)> on_load_callback_;
   std::set<td::Bits256> cells_to_migrate_;
   td::Timestamp migrate_after_ = td::Timestamp::never();
+  bool migration_active_ = false;
+
+  struct MigrationStats {
+    td::Timer start_;
+    td::Timestamp end_at_ = td::Timestamp::in(60.0);
+    size_t batches_ = 0;
+    size_t migrated_cells_ = 0;
+    size_t checked_cells_ = 0;
+    double total_time_ = 0.0;
+  };
+  std::unique_ptr<MigrationStats> migration_stats_;
+
+ public:
+  class MigrationProxy : public td::actor::Actor {
+   public:
+    explicit MigrationProxy(td::actor::ActorId<CellDbIn> cell_db) : cell_db_(cell_db) {
+    }
+    void migrate_cell(td::Bits256 hash) {
+      td::actor::send_closure(cell_db_, &CellDbIn::migrate_cell, hash);
+    }
+
+   private:
+    td::actor::ActorId<CellDbIn> cell_db_;
+  };
 };
 
 class CellDb : public CellDbBase {