diff --git a/core/core.services.yml b/core/core.services.yml
index 21a3e590ba..4b81990543 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -441,6 +441,8 @@ services:
   queue.database:
     class: Drupal\Core\Queue\QueueDatabaseFactory
     arguments: ['@database']
+  queue.memory:
+    class: Drupal\Core\Queue\QueueMemoryFactory
   path.alias_whitelist:
     class: Drupal\Core\Path\AliasWhitelist
     tags:
diff --git a/core/lib/Drupal/Core/Queue/QueueMemoryFactory.php b/core/lib/Drupal/Core/Queue/QueueMemoryFactory.php
new file mode 100644
index 0000000000..77c1abc293
--- /dev/null
+++ b/core/lib/Drupal/Core/Queue/QueueMemoryFactory.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Drupal\Core\Queue;
+
+/**
+ * Defines the factory for the memory backend.
+ */
+class QueueMemoryFactory {
+
+  /**
+   * Instantiated queues, keyed by name.
+   *
+   * @var array<string,Memory>
+   */
+  protected $queues = [];
+
+  /**
+   * Constructs a new queue object for a given name.
+   *
+   * @param string $name
+   *   The name of the queue
+   *
+   * @return \Drupal\Core\Queue\Memory
+   *   A memory store implementation for the given queue.
+   */
+  public function get($name) {
+    if (!isset($this->queues[$name])) {
+      $this->queues[$name] = new Memory($name);
+    }
+
+    return $this->queues[$name];
+  }
+
+}
diff --git a/core/modules/dblog/config/install/dblog.settings.yml b/core/modules/dblog/config/install/dblog.settings.yml
index 88add889ac..041ad6adb6 100644
--- a/core/modules/dblog/config/install/dblog.settings.yml
+++ b/core/modules/dblog/config/install/dblog.settings.yml
@@ -1 +1,5 @@
 row_limit: 1000
+queue:
+  enabled: false
+  level: 4
+
diff --git a/core/modules/dblog/config/schema/dblog.schema.yml b/core/modules/dblog/config/schema/dblog.schema.yml
index d7196da6da..fccaa15f0c 100644
--- a/core/modules/dblog/config/schema/dblog.schema.yml
+++ b/core/modules/dblog/config/schema/dblog.schema.yml
@@ -7,3 +7,14 @@ dblog.settings:
     row_limit:
       type: integer
       label: 'Database log messages to keep'
+    queue:
+      type: mapping
+      label: 'Queue-related settings'
+      mapping:
+        enabled:
+          type: boolean
+          label: 'Is the log queue enabled? This reduces the page delivery delay, but reduces log reliability, and is off by default.'
+        level:
+          type: integer
+          label: 'Maximum queued level. This is the PSR/3 level below which queue start flushing automatically.'
+          translatable: false
diff --git a/core/modules/dblog/dblog.module b/core/modules/dblog/dblog.module
index 700ceeaa6b..0c54336d75 100644
--- a/core/modules/dblog/dblog.module
+++ b/core/modules/dblog/dblog.module
@@ -10,8 +10,11 @@
  */
 
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Logger\RfcLogLevel;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\dblog\Form\DbLogSettingsFormPartial;
+use Drupal\dblog\Logger\DbLog;
 use Drupal\views\ViewEntityInterface;
 use Drupal\views\ViewExecutable;
 
@@ -61,7 +64,7 @@ function dblog_menu_links_discovered_alter(&$links) {
  */
 function dblog_cron() {
   // Cleanup the watchdog table.
-  $row_limit = \Drupal::config('dblog.settings')->get('row_limit');
+  $row_limit = \Drupal::config(DbLog::CONFIG_NAME)->get('row_limit');
 
   // For row limit n, get the wid of the nth row in descending wid order.
   // Counting the most recent n rows avoids issues with wid number sequences,
@@ -97,25 +100,9 @@ function _dblog_get_message_types() {
  * Implements hook_form_FORM_ID_alter() for system_logging_settings().
  */
 function dblog_form_system_logging_settings_alter(&$form, FormStateInterface $form_state) {
-  $row_limits = [100, 1000, 10000, 100000, 1000000];
-  $form['dblog_row_limit'] = [
-    '#type' => 'select',
-    '#title' => t('Database log messages to keep'),
-    '#default_value' => \Drupal::configFactory()->getEditable('dblog.settings')->get('row_limit'),
-    '#options' => [0 => t('All')] + array_combine($row_limits, $row_limits),
-    '#description' => t('The maximum number of messages to keep in the database log. Requires a <a href=":cron">cron maintenance task</a>.', [':cron' => \Drupal::url('system.status')])
-  ];
-
-  $form['#submit'][] = 'dblog_logging_settings_submit';
-}
-
-/**
- * Form submission handler for system_logging_settings().
- *
- * @see dblog_form_system_logging_settings_alter()
- */
-function dblog_logging_settings_submit($form, FormStateInterface $form_state) {
-  \Drupal::configFactory()->getEditable('dblog.settings')->set('row_limit', $form_state->getValue('dblog_row_limit'))->save();
+  /** @var DbLogSettingsFormPartial $formPartial */
+  $formPartial = \Drupal::classResolver()->getInstanceFromDefinition(DbLogSettingsFormPartial::class);
+  $form = $formPartial->buildForm($form, $form_state);
 }
 
 /**
diff --git a/core/modules/dblog/dblog.services.yml b/core/modules/dblog/dblog.services.yml
index 96ac2ee948..50daa48448 100644
--- a/core/modules/dblog/dblog.services.yml
+++ b/core/modules/dblog/dblog.services.yml
@@ -1,7 +1,12 @@
 services:
   logger.dblog:
     class: Drupal\dblog\Logger\DbLog
-    arguments: ['@database', '@logger.log_message_parser']
+    arguments:
+      - '@database'
+      - '@logger.log_message_parser'
+      - '@queue.memory'
+      - '@config.factory'
     tags:
       - { name: logger }
       - { name: backend_overridable }
+      - { name: event_subscriber }
diff --git a/core/modules/dblog/src/Form/DbLogSettingsFormPartial.php b/core/modules/dblog/src/Form/DbLogSettingsFormPartial.php
new file mode 100644
index 0000000000..ca1e965c85
--- /dev/null
+++ b/core/modules/dblog/src/Form/DbLogSettingsFormPartial.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace Drupal\dblog\Form;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Config\TypedConfigManagerInterface;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Logger\RfcLogLevel;
+use Drupal\dblog\Logger\DbLog;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+class DbLogSettingsFormPartial implements ContainerInjectionInterface {
+
+  /**
+   * The config factory service.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * The typed config service.
+   *
+   * @var \Drupal\Core\Config\TypedConfigManagerInterface
+   */
+  protected $configTyped;
+
+  /**
+   * DbLogSettingsFormPartial constructor.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
+   *   The config factory service.
+   * @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager
+   *   The typed config service.
+   */
+  public function __construct(ConfigFactoryInterface $configFactory, TypedConfigManagerInterface $typedConfigManager) {
+    $this->configFactory = $configFactory;
+    $this->configTyped = $typedConfigManager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    /** @var \Drupal\Core\Config\ConfigFactoryInterface $configFactory */
+    $configFactory = $container->get('config.factory');
+    /** @var \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager */
+    $typedConfigManager = $container->get('config.typed');
+    return new static($configFactory, $typedConfigManager);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $row_limits = [100, 1000, 10000, 100000, 1000000];
+    $config = $this->configFactory->getEditable(DbLog::CONFIG_NAME);
+    /** @var \Drupal\Core\TypedData\MapDataDefinition $definition */
+    $definition = $this->configTyped->get(DbLog::CONFIG_NAME)->getDataDefinition()['mapping']['queue'];
+    $mapping = $definition['mapping'];
+    $form['dblog'] = [
+      '#type' => 'details',
+      '#open' => TRUE,
+      '#title' => t($definition['label']),
+    ];
+
+    $form['dblog']['dblog_row_limit'] = [
+      '#type' => 'select',
+      '#title' => t('Database log messages to keep'),
+      '#default_value' => $config->get('row_limit'),
+      '#options' => [0 => t('All')] + array_combine($row_limits, $row_limits),
+      '#description' => t('The maximum number of messages to keep in the database log. Requires a <a href=":cron">cron maintenance task</a>.', [':cron' => \Drupal::url('system.status')])
+    ];
+
+    list($label, $description) = $this->parseLabel($mapping['enabled']['label']);
+    $form['dblog']['dblog_queue_enabled'] = [
+      '#type' => 'checkbox',
+      '#title' => t($label),
+      '#description' => t($description),
+      '#default_value' => $config->get('queue.enabled'),
+    ];
+
+    list($label, $description) = $this->parseLabel($mapping['level']['label']);
+    $form['dblog']['dblog_queue_level'] = [
+      '#title' => t($label),
+      '#description' => t($description),
+      '#type' => 'select',
+      '#options' => array_reverse(RfcLogLevel::getLevels()),
+      '#default_value' => $config->get('queue.level'),
+    ];
+
+    $form['#submit'][] = [$this, 'submitForm'];
+    return $form;
+  }
+
+  /**
+   * Build a label and description from the config schema label for consistency.
+   *
+   * @param string $schemaLabel
+   *   The schema label from typed configuration.
+   *
+   * @return array
+   *   A [label, description] array.
+   */
+  protected function parseLabel(string $schemaLabel) {
+    list($label, $delim, $rest) = preg_split('/([\.\?]) / ', $schemaLabel, 2, PREG_SPLIT_DELIM_CAPTURE);
+    $parsed = ["{$label}{$delim}", $rest];
+    return $parsed;
+  }
+
+  /**
+   * Form submission handler for system_logging_settings().
+   *
+   * @see dblog_form_system_logging_settings_alter()
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $this->configFactory->getEditable(DbLog::CONFIG_NAME)
+      ->set('row_limit', $form_state->getValue('dblog_row_limit'))
+      ->set('queue.enabled', $form_state->getValue('dblog_queue_enabled'))
+      ->set('queue.level', $form_state->getValue('dblog_queue_level'))
+      ->save();
+  }
+
+}
diff --git a/core/modules/dblog/src/Logger/DbLog.php b/core/modules/dblog/src/Logger/DbLog.php
index 97b0b7b305..9ced05d912 100644
--- a/core/modules/dblog/src/Logger/DbLog.php
+++ b/core/modules/dblog/src/Logger/DbLog.php
@@ -3,26 +3,43 @@
 namespace Drupal\dblog\Logger;
 
 use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Database\Connection;
 use Drupal\Core\Database\Database;
 use Drupal\Core\Database\DatabaseException;
 use Drupal\Core\DependencyInjection\DependencySerializationTrait;
 use Drupal\Core\Logger\LogMessageParserInterface;
 use Drupal\Core\Logger\RfcLoggerTrait;
+use Drupal\Core\Logger\RfcLogLevel;
+use Drupal\Core\Queue\QueueMemoryFactory;
 use Psr\Log\LoggerInterface;
+use Psr\Log\LogLevel;
+use Symfony\Component\EventDispatcher\Event;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpKernel\KernelEvents;
 
 /**
  * Logs events in the watchdog database table.
  */
-class DbLog implements LoggerInterface {
+class DbLog implements LoggerInterface, EventSubscriberInterface {
   use RfcLoggerTrait;
   use DependencySerializationTrait;
 
+  /**
+   * The name of the configuration used by this module.
+   */
+  const CONFIG_NAME = 'dblog.settings';
+
   /**
    * The dedicated database connection target to use for log entries.
    */
   const DEDICATED_DBLOG_CONNECTION_TARGET = 'dedicated_dblog';
 
+  /**
+   * The name of the memory queue used to defer database writes.
+   */
+  const QUEUE_NAME = 'dblog';
+
   /**
    * The database connection object.
    *
@@ -30,6 +47,13 @@ class DbLog implements LoggerInterface {
    */
   protected $connection;
 
+  /**
+   * The level below which queue gets flushed.
+   *
+   * @var string
+   */
+  protected $flushLevel;
+
   /**
    * The message's placeholders parser.
    *
@@ -37,6 +61,20 @@ class DbLog implements LoggerInterface {
    */
   protected $parser;
 
+  /**
+   * The deferred log queue.
+   *
+   * @var \Drupal\Core\Queue\Memory
+   */
+  protected $queue;
+
+  /**
+   * Does the loger use queueing?
+   *
+   * @var bool
+   */
+  protected $useQueue;
+
   /**
    * Constructs a DbLog object.
    *
@@ -44,23 +82,31 @@ class DbLog implements LoggerInterface {
    *   The database connection object.
    * @param \Drupal\Core\Logger\LogMessageParserInterface $parser
    *   The parser to use when extracting message variables.
+   * @param \Drupal\Core\Queue\QueueMemoryFactory $queueFactory
+   *   A queue factory service.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
+   *   The config.factory service.
    */
-  public function __construct(Connection $connection, LogMessageParserInterface $parser) {
+  public function __construct(Connection $connection, LogMessageParserInterface $parser, QueueMemoryFactory $queueFactory, ConfigFactoryInterface $configFactory) {
+    $config = $configFactory->get(static::CONFIG_NAME);
     $this->connection = $connection;
     $this->parser = $parser;
+    $this->useQueue = $config->get('queue.enabled');
+
+    // Conditionals.
+    $this->flushLevel = $this->useQueue ? $config->get('queue.level') : LogLevel::DEBUG;
+    $this->queue = $this->useQueue ? $queueFactory->get(static::QUEUE_NAME) : NULL;
   }
 
   /**
-   * {@inheritdoc}
+   * Write a single event to the database.
+   *
+   * @param int $level
+   * @param string $message
+   * @param array $message_placeholders
+   * @param array $context
    */
-  public function log($level, $message, array $context = []) {
-    // Remove any backtraces since they may contain an unserializable variable.
-    unset($context['backtrace']);
-
-    // Convert PSR3-style messages to SafeMarkup::format() style, so they can be
-    // translated too in runtime.
-    $message_placeholders = $this->parser->parseMessagePlaceholders($message, $context);
-
+  protected function dbLog($level, $message, array $message_placeholders, array $context = []) {
     try {
       $this->connection
         ->insert('watchdog')
@@ -109,4 +155,59 @@ public function log($level, $message, array $context = []) {
     }
   }
 
+  protected function dbLogQueue() {
+    // TODO convert to multi-value insert.
+    // TODO probably convert to something like cron->run() and a QueueWorker.
+    while ($item = $this->queue->claimItem()) {
+      call_user_func_array([$this, 'dbLog'], $item->data);
+      $this->queue->deleteItem($item);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    return [KernelEvents::TERMINATE => ['onTerminate']];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function log($level, $message, array $context = []) {
+    // Remove any backtraces since they may contain an unserializable variable.
+    unset($context['backtrace']);
+
+    // Convert PSR3-style messages to SafeMarkup::format() style, so they can be
+    // translated too in runtime.
+    $message_placeholders = $this->parser->parseMessagePlaceholders($message, $context);
+
+    if (!$this->useQueue) {
+      // ksm("Not queued write $message");
+      $this->dbLog($level, $message, $message_placeholders, $context);
+    }
+    elseif ($level <= $this->flushLevel) {
+      // ksm("Queued flush write $message");
+      // Log previously stored message.
+      $this->dbLogQueue();
+      // Then log this one to preserve ordering.
+      $this->dbLog($level, $message, $message_placeholders, $context);
+    }
+    else {
+      // ksm("Enqueueing $message");
+      $this->queue->createItem(compact('level', 'message', 'message_placeholders', 'context'));
+    }
+  }
+
+  /**
+   * Handler for KernelEvents::TERMINATE.
+   *
+   * @param \Symfony\Component\EventDispatcher\Event $event
+   */
+  public function onTerminate(Event $event) {
+    if ($this->useQueue) {
+      $this->dbLogQueue();
+    }
+  }
+
 }
diff --git a/core/modules/dblog/tests/src/Functional/DbLogTest.php b/core/modules/dblog/tests/src/Functional/DbLogTest.php
index 5b82282d6a..21d3c1c37c 100644
--- a/core/modules/dblog/tests/src/Functional/DbLogTest.php
+++ b/core/modules/dblog/tests/src/Functional/DbLogTest.php
@@ -7,6 +7,7 @@
 use Drupal\Core\Logger\RfcLogLevel;
 use Drupal\Core\Url;
 use Drupal\dblog\Controller\DbLogController;
+use Drupal\dblog\Logger\DbLog;
 use Drupal\Tests\BrowserTestBase;
 use Drupal\Tests\Traits\Core\CronRunTrait;
 
@@ -132,7 +133,7 @@ private function verifyRowLimit($row_limit) {
     $this->assertResponse(200);
 
     // Check row limit variable.
-    $current_limit = $this->config('dblog.settings')->get('row_limit');
+    $current_limit = $this->config(DbLog::CONFIG_NAME)->get('row_limit');
     $this->assertTrue($current_limit == $row_limit, format_string('[Cache] Row limit variable of @count equals row limit of @limit', ['@count' => $current_limit, '@limit' => $row_limit]));
   }
 
diff --git a/core/modules/dblog/tests/src/Kernel/Migrate/d6/MigrateDblogConfigsTest.php b/core/modules/dblog/tests/src/Kernel/Migrate/d6/MigrateDblogConfigsTest.php
index 73a6d83d22..62111912b5 100644
--- a/core/modules/dblog/tests/src/Kernel/Migrate/d6/MigrateDblogConfigsTest.php
+++ b/core/modules/dblog/tests/src/Kernel/Migrate/d6/MigrateDblogConfigsTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\dblog\Kernel\Migrate\d6;
 
+use Drupal\dblog\Logger\DbLog;
 use Drupal\Tests\SchemaCheckTestTrait;
 use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
 
@@ -31,9 +32,9 @@ protected function setUp() {
    * Tests migration of dblog variables to dblog.settings.yml.
    */
   public function testBookSettings() {
-    $config = $this->config('dblog.settings');
+    $config = $this->config(DbLog::CONFIG_NAME);
     $this->assertIdentical(10000, $config->get('row_limit'));
-    $this->assertConfigSchema(\Drupal::service('config.typed'), 'dblog.settings', $config->get());
+    $this->assertConfigSchema(\Drupal::service('config.typed'), DbLog::CONFIG_NAME, $config->get());
   }
 
 }
diff --git a/core/modules/dblog/tests/src/Kernel/Migrate/d7/MigrateDblogConfigsTest.php b/core/modules/dblog/tests/src/Kernel/Migrate/d7/MigrateDblogConfigsTest.php
index d9708309b2..882f3afbf9 100644
--- a/core/modules/dblog/tests/src/Kernel/Migrate/d7/MigrateDblogConfigsTest.php
+++ b/core/modules/dblog/tests/src/Kernel/Migrate/d7/MigrateDblogConfigsTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\dblog\Kernel\Migrate\d7;
 
+use Drupal\dblog\Logger\DbLog;
 use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
 
 /**
@@ -29,7 +30,7 @@ protected function setUp() {
    * Tests migration of dblog variables to dblog.settings.yml.
    */
   public function testDblogSettings() {
-    $config = $this->config('dblog.settings');
+    $config = $this->config(DbLog::CONFIG_NAME);
     $this->assertIdentical(10000, $config->get('row_limit'));
   }
 
