diff --git a/lib/modules/SSL.pmod/Connection.pike b/lib/modules/SSL.pmod/Connection.pike
index 59923928add4ae507abf12d4e5e4726091b7e9c6..d193c1f405e6fffa09d9cb3ba4846734e867521b 100644
--- a/lib/modules/SSL.pmod/Connection.pike
+++ b/lib/modules/SSL.pmod/Connection.pike
@@ -79,6 +79,8 @@ string(8bit) server_random;
 private constant Packet = .Packet;
 private constant Alert = .Alert;
 
+int(0..3) tickets_enabled = 0;
+
 // RFC 7301 (ALPN) 3.1:
 //   Unlike many other TLS extensions, this extension does not establish
 //   properties of the session, only of the connection.  When session
diff --git a/lib/modules/SSL.pmod/Context.pike b/lib/modules/SSL.pmod/Context.pike
index ad265a48c1febe74a286ee411bfee21837ad2e9e..5b232a6ab7af526ae755fa3f94f89642d8d05027 100644
--- a/lib/modules/SSL.pmod/Context.pike
+++ b/lib/modules/SSL.pmod/Context.pike
@@ -150,6 +150,9 @@ Alert alert_factory(object con,
 //!     Allows the client to select which of several domains hosted on
 //!     the same server it wants to connect to. Required by many
 //!     websites (@rfc{6066:3@}).
+//!   @value Constants.EXTENSION_session_ticket
+//!     Support session resumption without server-side state
+//!     (@rfc{4507@} and @rfc{5077@}).
 //!   @value Constants.EXTENSION_next_protocol_negotiation
 //!     Not supported by Pike. The server side will just check that
 //!     the client packets are correctly formatted.
@@ -190,6 +193,7 @@ multiset(int) extensions = (<
   EXTENSION_signature_algorithms,
   EXTENSION_elliptic_curves,
   EXTENSION_server_name,
+  EXTENSION_session_ticket,
   EXTENSION_next_protocol_negotiation,
   EXTENSION_signed_certificate_timestamp,
   EXTENSION_early_data,
@@ -1100,6 +1104,64 @@ Session lookup_session(string id)
     return 0;
 }
 
+//! Decode a session ticket and return the corresponding session
+//! if valid or zero if invalid.
+//!
+//! @note
+//!   The default implementation just calls @[lookup_session()].
+//!
+//!   Override this function (and @[encode_ticket()]) to implement
+//!   server-side state-less session resumption.
+//!
+//! @seealso
+//!   @[encode_ticket()], @[lookup_session()]
+Session decode_ticket(string(8bit) ticket)
+{
+  return lookup_session(ticket);
+}
+
+//! Generate a session ticket for a session.
+//!
+//! @note
+//!   The default implementation just generates a random ticket
+//!   and calls @[record_session()] to store it.
+//!
+//!   Over-ride this function (and @[decode_ticket()]) to implement
+//!   server-side state-less session resumption.
+//!
+//! @returns
+//!   Returns @expr{0@} (zero) on failure (ie cache disabled), and
+//!   an array on success:
+//!   @array
+//!     @elem string(8bit) 0
+//!       Non-empty string with the ticket.
+//!     @elem int
+//!       Lifetime hint for the ticket.
+//!   @endarray
+//!
+//! @seealso
+//!   @[decode_ticket()], @[record_session()], @rfc{4507:3.3@}
+array(string(8bit)|int) encode_ticket(Session session)
+{
+  if (!use_cache) return 0;
+  string(8bit) ticket = session->ticket;
+  if (!sizeof(ticket||"")) {
+    do {
+      ticket = random(32);
+    } while(session_cache[ticket]);
+    // FIXME: Should we update the fields here?
+    //        Consider moving this to the caller.
+    session->ticket = ticket;
+    session->ticket_expiry_time = time(1) + 3600;
+  }
+  string(8bit) orig_id = session->identity;
+  session->identity = ticket;
+  record_session(session);
+  session->identity = orig_id;
+  // FIXME: Calculate the lifetime from the ticket_expiry_time field?
+  return ({ ticket, 3600 });
+}
+
 //! Create a new session.
 Session new_session()
 {
@@ -1118,7 +1180,7 @@ Session new_session()
 //! Add a session to the cache (if caching is enabled).
 void record_session(Session s)
 {
-  if (use_cache && s->identity)
+  if (use_cache && sizeof(s->identity||""))
   {
     if( sizeof(session_cache) > max_sessions )
     {
diff --git a/lib/modules/SSL.pmod/ServerConnection.pike b/lib/modules/SSL.pmod/ServerConnection.pike
index 0faab53f1937cf5cc7a8d6a2bbbdcbd87d99270a..4fecdf6060c3a8799a84dd8a174b7a16ecda4743 100644
--- a/lib/modules/SSL.pmod/ServerConnection.pike
+++ b/lib/modules/SSL.pmod/ServerConnection.pike
@@ -109,6 +109,12 @@ protected Packet server_hello_packet()
     return Buffer();
   };
 
+  ext (EXTENSION_session_ticket, tickets_enabled) {
+    // RFC 4507 and RFC 5077.
+    SSL3_DEBUG_MSG("SSL.ServerConnection: Accepting session tickets.\n");
+    return Buffer();
+  };
+
   ext (EXTENSION_application_layer_protocol_negotiation, !!application_protocol)
   {
     return Buffer()->add_string_array(({application_protocol}), 1, 2);
@@ -171,6 +177,17 @@ protected Packet certificate_request_packet(Context context)
     return handshake_packet(HANDSHAKE_certificate_request, struct);
 }
 
+protected Packet new_session_ticket_packet(int lifetime_hint,
+					   string(8bit) ticket)
+{
+  SSL3_DEBUG_MSG("SSL.ServerConnection: New session ticket.\n");
+  Buffer struct = Buffer();
+  if (lifetime_hint < 0) lifetime_hint = 0;
+  struct->add_int(lifetime_hint, 4);
+  struct->add_hstring(ticket, 2);
+  return handshake_packet(HANDSHAKE_new_session_ticket, struct);
+}
+
 //! Renegotiate the connection (server initiated).
 //!
 //! Sends a @[hello_request] to force a new round of handshaking.
@@ -313,7 +330,7 @@ int(-1..1) handle_handshake(int type, Buffer input, Stdio.Buffer raw)
       if (type != HANDSHAKE_client_hello)
         COND_FATAL(1, ALERT_unexpected_message, "Expected client_hello.\n");
 
-      string session_id;
+      string(8bit) session_id;
       int cipher_len;
       array(int) cipher_suites;
       array(int) compression_methods;
@@ -556,6 +573,15 @@ int(-1..1) handle_handshake(int type, Buffer input, Stdio.Buffer raw)
                        "Invalid NPN extension.\n");
             break;
 
+	  case EXTENSION_session_ticket:
+	    SSL3_DEBUG_MSG("SSL.ServerConnection: Got session ticket.\n");
+	    tickets_enabled = 1;
+	    // NB: RFC 4507 and 5077 differ in encoding here.
+	    //     Apparently no implementations actually followed
+	    //     the RFC 4507 encoding.
+	    session->ticket = extension_data->read();
+	    break;
+
           case EXTENSION_signed_certificate_timestamp:
             COND_FATAL(sizeof(extension_data), ALERT_handshake_failure,
                        "Invalid signed certificate timestamp extension.\n");
@@ -788,17 +814,41 @@ int(-1..1) handle_handshake(int type, Buffer input, Stdio.Buffer raw)
                    ALERT_illegal_parameter,
                    "Illegal with compression in TLS 1.3 and later.\n");
 
-        Session old_session = sizeof(session_id) &&
-          context->lookup_session(session_id);
-        if (old_session && old_session->reusable_as(session))
-        {
-          SSL3_DEBUG_MSG("SSL.ServerConnection: Reusing session %O\n",
-                         session_id);
-
-          /* Reuse session */
-          session = old_session;
-          reuse = 1;
-        }
+	Session old_session;
+	if (tickets_enabled) {
+	  SSL3_DEBUG_MSG("SSL.ServerConnection: Decoding ticket: %O...\n",
+			 session->ticket);
+	  old_session = sizeof(session->ticket) &&
+	    context->decode_ticket(session->ticket);
+
+	  // RFC 4507 3.4:
+	  //   If a server is planning on issuing a SessionTicket to a
+	  //   client that does not present one, it SHOULD include an
+	  //   empty Session ID in the ServerHello. If the server
+	  //   includes a non-empty session ID, then it is indicating
+	  //   intent to use stateful session resume.
+	  //[...]
+	  //   If the server accepts the ticket and the Session ID is
+	  //   not empty, then it MUST respond with the same Session
+	  //   ID present in the ClientHello.
+
+	  session->identity = "";
+	  if (old_session) {
+	    old_session->identity = session_id;
+	  }
+	} else {
+	  old_session = sizeof(session_id) &&
+	    context->lookup_session(session_id);
+	}
+	if (old_session && old_session->reusable_as(session))
+	{
+	  SSL3_DEBUG_MSG("SSL.ServerConnection: Reusing session %O\n",
+			 session_id);
+
+	  /* Reuse session */
+	  session = old_session;
+	  reuse = 1;
+	}
 
         /* TLS 1.3 or later.
          *
@@ -871,12 +921,36 @@ int(-1..1) handle_handshake(int type, Buffer input, Stdio.Buffer raw)
 
       session->set_compression_method(compression_methods[0]);
 
+      Session old_session;
+      if (tickets_enabled) {
+	SSL3_DEBUG_MSG("SSL.ServerConnection: Decoding ticket: %O...\n",
+		       session->ticket);
+	old_session = sizeof(session->ticket) &&
+	  context->decode_ticket(session->ticket);
+
+	// RFC 4507 3.4:
+	//   If a server is planning on issuing a SessionTicket to a
+	//   client that does not present one, it SHOULD include an
+	//   empty Session ID in the ServerHello. If the server
+	//   includes a non-empty session ID, then it is indicating
+	//   intent to use stateful session resume.
+	//[...]
+	//   If the server accepts the ticket and the Session ID is
+	//   not empty, then it MUST respond with the same Session
+	//   ID present in the ClientHello.
+
+	session->identity = "";
+	if (old_session) {
+	  old_session->identity = session_id;
+	}
+      } else {
 #ifdef SSL3_DEBUG
-      if (sizeof(session_id))
-        werror("SSL.ServerConnection: Looking up session %O\n", session_id);
+	if (sizeof(session_id))
+	  werror("SSL.ServerConnection: Looking up session %O\n", session_id);
 #endif
-      Session old_session = sizeof(session_id) &&
-        context->lookup_session(session_id);
+	old_session = sizeof(session_id) &&
+	  context->lookup_session(session_id);
+      }
       if (old_session && old_session->reusable_as(session))
       {
         SSL3_DEBUG_MSG("SSL.ServerConnection: Reusing session %O\n",
@@ -886,6 +960,13 @@ int(-1..1) handle_handshake(int type, Buffer input, Stdio.Buffer raw)
         session = old_session;
         send_packet(server_hello_packet());
 
+	if (tickets_enabled) {
+	  SSL3_DEBUG_MSG("SSL.ServerConnection: Resending ticket.\n");
+	  int lifetime_hint = [int](session->ticket_expiration_time - time(1));
+	  string(8bit) ticket = session->ticket;
+	  send_packet(new_session_ticket_packet(lifetime_hint, ticket));
+	}
+
         new_cipher_states();
         send_packet(change_cipher_packet());
         if(version == PROTOCOL_SSL_3_0)
@@ -1180,6 +1261,18 @@ int(-1..1) handle_handshake(int type, Buffer input, Stdio.Buffer raw)
       if (!reuse)
       {
 	if (version < PROTOCOL_TLS_1_3) {
+	  if (tickets_enabled) {
+	    array(string(8bit)|int) ticket_info =
+	      context->encode_ticket(session);
+	    if (ticket_info) {
+	      SSL3_DEBUG_MSG("SSL.ServerConnection: Sending ticket %O.\n",
+			     ticket_info);
+	      session->ticket = [string(8bit)](ticket_info[0]);
+	      session->ticket_expiry_time = [int](ticket_info[1] + time(1));
+	      send_packet(new_session_ticket_packet([int](ticket_info[1]),
+						    [string(8bit)](ticket_info[0])));
+	    }
+	  }
 	  send_packet(change_cipher_packet());
 	  // We've already received the CCS from the peer.
 	  expect_change_cipher--;
diff --git a/lib/modules/SSL.pmod/Session.pike b/lib/modules/SSL.pmod/Session.pike
index 5aeb12e946c47cc0b6a24fd344caf52420fb78fe..54b1812f7e2d8a3fe339bed46ce871546694f5b2 100644
--- a/lib/modules/SSL.pmod/Session.pike
+++ b/lib/modules/SSL.pmod/Session.pike
@@ -24,6 +24,14 @@ int last_activity = time();
 //! Identifies the session to the server
 string(8bit) identity;
 
+//! Alternative identification of the session to the server.
+//! @seealso
+//!   @rfc{4507@}, @rfc{5077@}
+string(8bit) ticket;
+
+//! Expiry time for @[ticket].
+int ticket_expiry_time;
+
 //! Always COMPRESSION_null.
 int compression_algorithm;