diff --git a/src/box/box.cc b/src/box/box.cc
index d146ec16284b382ede9033b4dc2429162b0fdcab..121ad787d59b3cab1ceaea964de5cefec05522a1 100644
--- a/src/box/box.cc
+++ b/src/box/box.cc
@@ -89,11 +89,6 @@ static void title(const char *new_status)
 
 const struct vclock *box_vclock = &replicaset.vclock;
 
-/**
- * Set if there's a fiber performing box_checkpoint() right now.
- */
-static bool checkpoint_is_in_progress;
-
 /**
  * Set if backup is in progress, i.e. box_backup_start() was
  * called but box_backup_stop() hasn't been yet.
@@ -2185,45 +2180,8 @@ box_checkpoint()
 	/* Signal arrived before box.cfg{} */
 	if (! is_box_configured)
 		return 0;
-	int rc = 0;
-	if (checkpoint_is_in_progress) {
-		diag_set(ClientError, ER_CHECKPOINT_IN_PROGRESS);
-		return -1;
-	}
-	checkpoint_is_in_progress = true;
-	/* create checkpoint files */
-	latch_lock(&schema_lock);
-	if ((rc = engine_begin_checkpoint()))
-		goto end;
-
-	struct vclock vclock;
-	if ((rc = wal_begin_checkpoint(&vclock)))
-		goto end;
 
-	if ((rc = engine_commit_checkpoint(&vclock)))
-		goto end;
-
-	wal_commit_checkpoint(&vclock);
-	gc_add_checkpoint(&vclock);
-end:
-	if (rc)
-		engine_abort_checkpoint();
-
-	latch_unlock(&schema_lock);
-	checkpoint_is_in_progress = false;
-
-	/*
-	 * Wait for background garbage collection that might
-	 * have been triggered by this checkpoint to complete.
-	 * Strictly speaking, it isn't necessary, but it
-	 * simplifies testing as it guarantees that by the
-	 * time box.snapshot() returns, all outdated checkpoint
-	 * files have been removed.
-	 */
-	if (!rc)
-		gc_wait();
-
-	return rc;
+	return gc_checkpoint();
 }
 
 int
diff --git a/src/box/gc.c b/src/box/gc.c
index 87273b8ddf63690e4c2050581f7c8eae190742b6..153ef65ce8b682b122c5c938736bff78c5251c2a 100644
--- a/src/box/gc.c
+++ b/src/box/gc.c
@@ -45,11 +45,14 @@
 #include <small/rlist.h>
 
 #include "diag.h"
+#include "errcode.h"
 #include "fiber.h"
 #include "fiber_cond.h"
+#include "latch.h"
 #include "say.h"
 #include "vclock.h"
 #include "cbus.h"
+#include "schema.h"
 #include "engine.h"		/* engine_collect_garbage() */
 #include "wal.h"		/* wal_collect_garbage() */
 
@@ -242,7 +245,11 @@ gc_schedule(void)
 		fiber_wakeup(gc.fiber);
 }
 
-void
+/**
+ * Wait for background garbage collection scheduled prior
+ * to this point to complete.
+ */
+static void
 gc_wait(void)
 {
 	unsigned scheduled = gc.scheduled;
@@ -320,6 +327,65 @@ gc_add_checkpoint(const struct vclock *vclock)
 	gc_schedule();
 }
 
+int
+gc_checkpoint(void)
+{
+	int rc;
+	struct vclock vclock;
+
+	if (gc.checkpoint_is_in_progress) {
+		diag_set(ClientError, ER_CHECKPOINT_IN_PROGRESS);
+		return -1;
+	}
+	gc.checkpoint_is_in_progress = true;
+
+	/*
+	 * We don't support DDL operations while making a checkpoint.
+	 * Lock them out.
+	 */
+	latch_lock(&schema_lock);
+
+	/*
+	 * Rotate WAL and call engine callbacks to create a checkpoint
+	 * on disk for each registered engine.
+	 */
+	rc = engine_begin_checkpoint();
+	if (rc != 0)
+		goto out;
+	rc = wal_begin_checkpoint(&vclock);
+	if (rc != 0)
+		goto out;
+	rc = engine_commit_checkpoint(&vclock);
+	if (rc != 0)
+		goto out;
+	wal_commit_checkpoint(&vclock);
+
+	/*
+	 * Finally, track the newly created checkpoint in the garbage
+	 * collector state.
+	 */
+	gc_add_checkpoint(&vclock);
+out:
+	if (rc != 0)
+		engine_abort_checkpoint();
+
+	latch_unlock(&schema_lock);
+	gc.checkpoint_is_in_progress = false;
+
+	/*
+	 * Wait for background garbage collection that might
+	 * have been triggered by this checkpoint to complete.
+	 * Strictly speaking, it isn't necessary, but it
+	 * simplifies testing as it guarantees that by the
+	 * time box.snapshot() returns, all outdated checkpoint
+	 * files have been removed.
+	 */
+	if (rc == 0)
+		gc_wait();
+
+	return rc;
+}
+
 void
 gc_ref_checkpoint(struct gc_checkpoint *checkpoint,
 		  struct gc_checkpoint_ref *ref, const char *format, ...)
diff --git a/src/box/gc.h b/src/box/gc.h
index a141ace680dada047c055735fdaeba3fb0fc240a..84a441f2684a55677b6780e26dc6c93488fa331f 100644
--- a/src/box/gc.h
+++ b/src/box/gc.h
@@ -31,6 +31,7 @@
  * SUCH DAMAGE.
  */
 
+#include <stdbool.h>
 #include <stddef.h>
 #include <small/rlist.h>
 
@@ -141,6 +142,10 @@ struct gc_state {
 	 * taken at that moment of time.
 	 */
 	unsigned completed, scheduled;
+	/**
+	 * Set if there's a fiber making a checkpoint right now.
+	 */
+	bool checkpoint_is_in_progress;
 };
 extern struct gc_state gc;
 
@@ -191,13 +196,6 @@ gc_init(void);
 void
 gc_free(void);
 
-/**
- * Wait for background garbage collection scheduled prior
- * to this point to complete.
- */
-void
-gc_wait(void);
-
 /**
  * Advance the garbage collector vclock to the given position.
  * Deactivate WAL consumers that need older data.
@@ -217,13 +215,25 @@ void
 gc_set_min_checkpoint_count(int min_checkpoint_count);
 
 /**
- * Track a new checkpoint in the garbage collector state.
- * Note, this function may run garbage collector to remove
+ * Track an existing checkpoint in the garbage collector state.
+ * Note, this function may trigger garbage collection to remove
  * old checkpoints.
  */
 void
 gc_add_checkpoint(const struct vclock *vclock);
 
+/**
+ * Make a checkpoint.
+ *
+ * This function runs engine/WAL callbacks to create a checkpoint
+ * on disk, then tracks the new checkpoint in the garbage collector
+ * state (see gc_add_checkpoint()).
+ *
+ * Returns 0 on success. On failure returns -1 and sets diag.
+ */
+int
+gc_checkpoint(void);
+
 /**
  * Get a reference to @checkpoint and store it in @ref.
  * This will block the garbage collector from deleting