diff --git a/graf2d/gpad/v7/inc/ROOT/TCanvas.hxx b/graf2d/gpad/v7/inc/ROOT/TCanvas.hxx
index 31a0fe9cf91c2fe94ae1e4e0fc2be52f4ffa13a5..5be2df35eb3672d6415b515756a571fe15ef85a8 100644
--- a/graf2d/gpad/v7/inc/ROOT/TCanvas.hxx
+++ b/graf2d/gpad/v7/inc/ROOT/TCanvas.hxx
@@ -136,7 +136,7 @@ public:
    bool IsModified() const;
 
    /// update drawing
-   void Update(bool async = false);
+   void Update(bool async = false, CanvasCallback_t callback = nullptr);
 
    /// Save canvas in image file
    void SaveAs(const std::string &filename, bool async = false, CanvasCallback_t callback = nullptr);
diff --git a/graf2d/gpad/v7/inc/ROOT/TVirtualCanvasPainter.hxx b/graf2d/gpad/v7/inc/ROOT/TVirtualCanvasPainter.hxx
index c9960f985600bd4c08328c9517a8297c26b0fad0..fdc4f5047ed62c9d12a5b9625e1c4674a6e3de97 100644
--- a/graf2d/gpad/v7/inc/ROOT/TVirtualCanvasPainter.hxx
+++ b/graf2d/gpad/v7/inc/ROOT/TVirtualCanvasPainter.hxx
@@ -57,7 +57,7 @@ public:
    virtual void AddDisplayItem(TDisplayItem *item) = 0;
 
    /// indicate that canvas changed, provides current version of the canvas
-   virtual void CanvasUpdated(uint64_t, bool) = 0;
+   virtual void CanvasUpdated(uint64_t, bool, CanvasCallback_t) = 0;
 
    /// return true if canvas modified since last painting
    virtual bool IsCanvasModified(uint64_t) const = 0;
diff --git a/graf2d/gpad/v7/src/TCanvas.cxx b/graf2d/gpad/v7/src/TCanvas.cxx
index 07287e18fa6d859b0a60a904ed0045a5f2e565e4..fd00696d70bc709ad532f8da376e8823aa354eac 100644
--- a/graf2d/gpad/v7/src/TCanvas.cxx
+++ b/graf2d/gpad/v7/src/TCanvas.cxx
@@ -48,10 +48,10 @@ bool ROOT::Experimental::TCanvas::IsModified() const
    return fPainter ? fPainter->IsCanvasModified(fModified) : fModified;
 }
 
-void ROOT::Experimental::TCanvas::Update(bool async)
+void ROOT::Experimental::TCanvas::Update(bool async, CanvasCallback_t callback)
 {
    if (fPainter)
-      fPainter->CanvasUpdated(fModified, async);
+      fPainter->CanvasUpdated(fModified, async, callback);
 
    // SnapshotList_t lst;
    // for (auto&& drw: fPrimitives) {
@@ -97,7 +97,7 @@ void ROOT::Experimental::TCanvas::Show(const std::string &where)
    fPainter = Internal::TVirtualCanvasPainter::Create(*this, batch_mode);
    if (fPainter) {
       fPainter->NewDisplay(where);
-      fPainter->CanvasUpdated(fModified, true); // trigger async display
+      fPainter->CanvasUpdated(fModified, true, nullptr); // trigger async display
    }
 }
 
diff --git a/gui/canvaspainter/v7/src/TCanvasPainter.cxx b/gui/canvaspainter/v7/src/TCanvasPainter.cxx
index c1ad9e4ea201bc3e73994bb8b50d89e493b3182c..e175879e575721ad8e88667226afaeb1005b1520 100644
--- a/gui/canvaspainter/v7/src/TCanvasPainter.cxx
+++ b/gui/canvaspainter/v7/src/TCanvasPainter.cxx
@@ -173,18 +173,26 @@ private:
    };
 
    struct WebCommand {
-      std::string fId;       ///<! command identifier
-      std::string fName;     ///<! command name
-      std::string fArg;      ///<! command arg
-      bool fRunning;         ///<! true when command submitted
+      std::string fId;                                ///<! command identifier
+      std::string fName;                              ///<! command name
+      std::string fArg;                               ///<! command arg
+      bool fRunning;                                  ///<! true when command submitted
       ROOT::Experimental::CanvasCallback_t fCallback; ///<! callback function associated with command
       WebCommand() : fId(), fName(), fArg(), fRunning(false), fCallback() {}
    };
 
+   struct WebUpdate {
+      uint64_t fVersion;                              ///<! canvas version
+      ROOT::Experimental::CanvasCallback_t fCallback; ///<! callback function associated with command
+      WebUpdate() : fVersion(0), fCallback() {}
+   };
+
    typedef std::list<WebConn> WebConnList;
 
    typedef std::list<WebCommand> WebCommandsList;
 
+   typedef std::list<WebUpdate> WebUpdatesList;
+
    typedef std::vector<ROOT::Experimental::Detail::TMenuItem> MenuItemsVector;
 
    /// The canvas we are painting. It might go out of existence while painting.
@@ -201,6 +209,7 @@ private:
    uint64_t fSnapshotVersion;   ///!< version of snapshot
    std::string fSnapshot;       ///!< last produced snapshot
    uint64_t fSnapshotDelivered; ///!< minimal version delivered to all connections
+   WebUpdatesList fUpdatesLst;  ///!< list of callbacks for canvas update
 
    static std::string fAddr;    ///<! real http address (when assigned)
    static THttpServer *gServer; ///<! server
@@ -234,7 +243,7 @@ public:
    /// The painter observes it; it needs to know should the TCanvas be deleted.
    TCanvasPainter(const std::string &name, const ROOT::Experimental::TCanvas &canv, bool batch_mode)
       : THttpWSHandler(name.c_str(), "title"), fCanvas(canv), fBatchMode(batch_mode), fWebConn(), fDisplayList(),
-        fCmds(), fCmdsCnt(0), fWaitingCmdId(), fSnapshotVersion(0), fSnapshot(), fSnapshotDelivered(0)
+        fCmds(), fCmdsCnt(0), fWaitingCmdId(), fSnapshotVersion(0), fSnapshot(), fSnapshotDelivered(0), fUpdatesLst()
    {
       CreateHttpServer();
       gServer->Register("/web7gui", this);
@@ -244,12 +253,13 @@ public:
 
    virtual void AddDisplayItem(ROOT::Experimental::TDisplayItem *item) final;
 
-   virtual void CanvasUpdated(uint64_t, bool) override;
+   virtual void CanvasUpdated(uint64_t, bool, ROOT::Experimental::CanvasCallback_t) override;
 
    virtual bool IsCanvasModified(uint64_t) const override;
 
    /// perform special action when drawing is ready
-   virtual void DoWhenReady(const std::string &cmd, const std::string &arg, bool async, ROOT::Experimental::CanvasCallback_t callback) final;
+   virtual void DoWhenReady(const std::string &cmd, const std::string &arg, bool async,
+                            ROOT::Experimental::CanvasCallback_t callback) final;
 
    // open new display for the canvas
    virtual void NewDisplay(const std::string &where) override;
@@ -380,8 +390,8 @@ void TCanvasPainter::NewDisplay(const std::string &where)
 
    TString exec;
 
-   if (!is_native && !ic_cef && !is_qt5 && (where!="browser")) {
-      if (where.find("$url")!=std::string::npos) {
+   if (!is_native && !ic_cef && !is_qt5 && (where != "browser")) {
+      if (where.find("$url") != std::string::npos) {
          exec = where.c_str();
          exec.ReplaceAll("$url", addr);
       } else {
@@ -397,13 +407,26 @@ void TCanvasPainter::NewDisplay(const std::string &where)
    gSystem->Exec(exec);
 }
 
-void TCanvasPainter::CanvasUpdated(uint64_t ver, bool async)
+void TCanvasPainter::CanvasUpdated(uint64_t ver, bool async, ROOT::Experimental::CanvasCallback_t callback)
 {
+   if (ver && fSnapshotDelivered && (ver <= fSnapshotDelivered)) {
+      // if given canvas version was already delivered to clients, can return immediately
+      if (callback) callback(true);
+      return;
+   }
+
    fSnapshotVersion = ver;
    fSnapshot = CreateSnapshot(fCanvas);
 
    CheckDataToSend();
 
+   if (callback) {
+      WebUpdate item;
+      item.fVersion = ver;
+      item.fCallback = callback;
+      fUpdatesLst.push_back(item);
+   }
+
    if (!async)
       WaitWhenCanvasPainted(ver);
 }
@@ -431,7 +454,8 @@ bool TCanvasPainter::WaitWhenCanvasPainted(uint64_t ver)
    return false;
 }
 
-void TCanvasPainter::DoWhenReady(const std::string &name, const std::string &arg, bool async, ROOT::Experimental::CanvasCallback_t callback)
+void TCanvasPainter::DoWhenReady(const std::string &name, const std::string &arg, bool async,
+                                 ROOT::Experimental::CanvasCallback_t callback)
 {
    if (!async && !fWaitingCmdId.empty()) {
       Error("DoWhenReady", "Fail to submit sync command when previous is still awaited - use async");
@@ -446,11 +470,13 @@ void TCanvasPainter::DoWhenReady(const std::string &name, const std::string &arg
    cmd.fCallback = callback;
    fCmds.push_back(cmd);
 
-   if (!async) fWaitingCmdId = cmd.fId;
+   if (!async)
+      fWaitingCmdId = cmd.fId;
 
    CheckDataToSend();
 
-   if (async) return;
+   if (async)
+      return;
 
    uint64_t cnt = 0;
    bool had_connection = false;
@@ -467,7 +493,6 @@ void TCanvasPainter::DoWhenReady(const std::string &name, const std::string &arg
       gSystem->ProcessEvents();
       gSystem->Sleep((++cnt < 500) ? 1 : 100); // increase sleep interval when do very often
    }
-
 }
 
 /////////////////////////////////////////////////////////////////////////////////////////////
@@ -476,12 +501,15 @@ void TCanvasPainter::DoWhenReady(const std::string &name, const std::string &arg
 
 void TCanvasPainter::PopFrontCommand(bool result)
 {
-   if (fCmds.size() == 0) return;
+   if (fCmds.size() == 0)
+      return;
 
    // simple condition, which will be checked in waiting loop
-   if (!fWaitingCmdId.empty() && (fWaitingCmdId == fCmds.front().fId)) fWaitingCmdId.clear();
+   if (!fWaitingCmdId.empty() && (fWaitingCmdId == fCmds.front().fId))
+      fWaitingCmdId.clear();
 
-   if (fCmds.front().fCallback) fCmds.front().fCallback(result);
+   if (fCmds.front().fCallback)
+      fCmds.front().fCallback(result);
 
    fCmds.pop_front();
 }
@@ -619,7 +647,8 @@ Bool_t TCanvasPainter::ProcessWS(THttpCallArg *arg)
       const char *sid = cdata + 6;
       const char *separ = strchr(sid, ':');
       std::string id;
-      if (separ) id.append(sid, separ - sid);
+      if (separ)
+         id.append(sid, separ - sid);
       if (fCmds.size() == 0) {
          Error("ProcessWS", "Get REPLY without command");
       } else if (!fCmds.front().fRunning) {
@@ -627,7 +656,7 @@ Bool_t TCanvasPainter::ProcessWS(THttpCallArg *arg)
       } else if (fCmds.front().fId != id) {
          Error("ProcessWS", "Mismatch with front command and ID in REPLY");
       } else {
-         bool res = FrontCommandReplied(separ+1);
+         bool res = FrontCommandReplied(separ + 1);
          PopFrontCommand(res);
       }
       conn->fReady = kTRUE;
@@ -666,7 +695,7 @@ void TCanvasPainter::CheckDataToSend()
 
       TString buf;
 
-      if (conn.fDrawReady && (fCmds.size()>0) && !fCmds.front().fRunning) {
+      if (conn.fDrawReady && (fCmds.size() > 0) && !fCmds.front().fRunning) {
          WebCommand &cmd = fCmds.front();
          cmd.fRunning = true;
          buf = "CMD:";
@@ -710,6 +739,16 @@ void TCanvasPainter::CheckDataToSend()
 
    if (fSnapshotDelivered != min_delivered) {
       fSnapshotDelivered = min_delivered;
+
+      auto iter = fUpdatesLst.begin();
+      while (iter != fUpdatesLst.end()) {
+         auto curr = iter; iter++;
+         if (curr->fVersion <= fSnapshotDelivered) {
+            curr->fCallback(true);
+            fUpdatesLst.erase(curr);
+         }
+      }
+
       // one could call-back canvas methods here
    }
 }
diff --git a/tutorials/v7/draw_v6.cxx b/tutorials/v7/draw_v6.cxx
index 945560e35ffded48983720f689dec2876097ef96..bfd6ec45dbf2ce25fb98b64bbe328deed65e8920 100644
--- a/tutorials/v7/draw_v6.cxx
+++ b/tutorials/v7/draw_v6.cxx
@@ -40,7 +40,11 @@ void draw_v6()
    // canvas->Show("/usr/bin/opera");  // one could specify program name which should show canvas
    // canvas->Show("firefox");         // it could be firefox, opera, chromium; canvas can be shown several times
 
-   canvas->Update(); // synchronous, wait until painting is finished
+   // synchronous, wait until painting is finished
+   canvas->Update(false, [](bool res) { std::cout << "First Update done = " << (res ? "true" : "false") << std::endl; });
+
+   // call again, should return immediately
+   canvas->Update(false, [](bool res) { std::cout << "Second Update done = " << (res ? "true" : "false") << std::endl; });
 
    // request to create PNG file in asynchronous mode and specify lambda function as callback
    // when request processed by the client, callback called with result value