diff --git a/demo/server_demo.c b/demo/server_demo.c
index 9168dc5..98b2127 100644
--- a/demo/server_demo.c
+++ b/demo/server_demo.c
@@ -81,17 +81,29 @@ httpd_cgi_state templateReplacer(HttpdConnData *conn, const char *token)
 //    httpdGetHeader(conn, "Cookie", )
 //}
 
+
+
 httpd_cgi_state cgiStartSession(HttpdConnData *conn)
 {
     httpdQueueHeader(conn, "X-Foo", "FOO");
     httpdQueueHeader(conn, "X-Bar", "Bar");
 
+    httpdSetCookie(conn, &(SetCookie) {
+        .name = "testCookie",
+        .value = "lala-lele",
+//        .domain = "localhost",
+//        .expires = "BABABABAx",
+//        .httponly = true,
+        .maxAge = -1,
+//        .path = "foo/bar/baz",
+//        .sameSite = COOKIE_SAMESITE_LAX,
+//        .secure = true,
+    });
+
     return HTTPD_CGI_NOTFOUND;
 }
 
 
-
-
 /**
  * Application routes
  */
diff --git a/spritehttpd/include/httpd-types.h b/spritehttpd/include/httpd-types.h
index e900085..a6ed65f 100644
--- a/spritehttpd/include/httpd-types.h
+++ b/spritehttpd/include/httpd-types.h
@@ -10,6 +10,10 @@
 
 #include "httpd-config.h"
 
+
+// TODO error enum
+
+
 // opaque conn type struct
 struct HttpdConnType;
 typedef struct HttpdConnType HttpdConnType;
@@ -67,6 +71,34 @@ struct httpd_init_options {
   uint16_t port;
 };
 
+/**
+ * Parameters for Set-Cookie
+ *
+ * Leave unused fields as NULL or false for defaults.
+ *
+ * If "maxAge" is zero, it's not sent, setting a session cookie.
+ * This is to allow leaving the field unset by default.
+ *
+ * To expire a cookie immediately, use maxAge = -1
+ */
+typedef struct SetCookie {
+  const char *name;
+  const char *value;
+  const char *expires;
+  const int32_t maxAge;
+  const char *domain;
+  const char *path;
+  const char *sameSite;
+  const bool secure;
+  const bool httponly;
+} SetCookie;
+
+// Supported values for the "sameSite" attribute of SetCookie.
+// Any string can be sent, these are the standard forms suspported by browsers.
+#define COOKIE_SAMESITE_LAX    "Lax"
+#define COOKIE_SAMESITE_STRICT "Strict"
+#define COOKIE_SAMESITE_NONE   "None"
+
 
 /* Private types - exposed to allow static alloc */
 
@@ -100,6 +132,14 @@ struct HttpdPriv {
   uint8_t flags;
 };
 
+//Flags (1 byte)
+#define HFL_HTTP11          (1<<0)
+#define HFL_CHUNKED         (1<<1)
+#define HFL_SENDINGBODY     (1<<2)
+#define HFL_DISCONAFTERSENT (1<<3)
+#define HFL_NOCONNECTIONSTR (1<<4)
+#define HFL_NOCORS          (1<<5)
+
 /// Callback type that releases the user data attached to the connection.
 /// The format is compatible with regular "free()" or
 typedef void (* httpdUserDataCleanupCb)(void *userData);
diff --git a/spritehttpd/include/httpd.h b/spritehttpd/include/httpd.h
index c3c2ea6..9155d92 100644
--- a/spritehttpd/include/httpd.h
+++ b/spritehttpd/include/httpd.h
@@ -10,7 +10,6 @@
 #include "httpd-routes.h"
 #include "httpd-config.h"
 
-
 /**
  * Get the server version string
  *
@@ -113,6 +112,27 @@ int httpdGetHeader(HttpdConnData *conn, const char *header, char *buff, size_t b
  */
 void httpdQueueHeader(HttpdConnData *conn, const char *header, const char *value);
 
+/**
+ * Queue a header to be sent with the response. This is a variant of `httpdQueueHeader()`
+ * which uses the raw internal struct to avoid re-alloc.
+ *
+ * The variable length field of the struct must contain `HeaderName: HeaderValue + CR LF`
+ *
+ * @param conn
+ * @param queEntry
+ * @return 1 = OK
+ */
+bool httpdQueueHeaderRaw(HttpdConnData *conn, HttpdQueuedHeader *queEntry);
+
+/**
+ * Set cookie. This queues the cookie header.
+ *
+ * @param conn
+ * @param parm
+ * @return
+ */
+bool httpdSetCookie(HttpdConnData *conn, const SetCookie *parm);
+
 /**
  * Send binary data
  *
diff --git a/spritehttpd/src/httpd.c b/spritehttpd/src/httpd.c
index e47f9ee..06cdb30 100644
--- a/spritehttpd/src/httpd.c
+++ b/spritehttpd/src/httpd.c
@@ -29,14 +29,6 @@ _Static_assert(HTTPD_MAX_CONNECTIONS < 256, "HTTPD_MAX_CONNECTIONS must be at mo
 static const HttpdBuiltInUrl *s_builtInUrls;
 static const char *s_serverName = HTTPD_SERVERNAME;
 
-//Flags (1 byte)
-#define HFL_HTTP11 (1<<0)
-#define HFL_CHUNKED (1<<1)
-#define HFL_SENDINGBODY (1<<2)
-#define HFL_DISCONAFTERSENT (1<<3)
-#define HFL_NOCONNECTIONSTR (1<<4)
-#define HFL_NOCORS (1<<5)
-
 
 
 //Connection pool
@@ -198,6 +190,23 @@ void httpdQueueHeader(HttpdConnData *conn, const char *header, const char *value
     strcat(queEntry->headerLine, value);
     strcat(queEntry->headerLine, "\r\n");
 
+    if (!httpdQueueHeaderRaw(conn, queEntry)) {
+        httpdFree(queEntry);
+    }
+}
+
+bool httpdQueueHeaderRaw(HttpdConnData *conn, HttpdQueuedHeader *queEntry)
+{
+    if (!conn || !queEntry) {
+        return false;
+    }
+    if (conn->priv.flags & HFL_SENDINGBODY) {
+        http_error("Headers already sent.");
+        return false;
+    }
+
+    queEntry->next = NULL;
+
     if (!conn->priv.headersToSend) {
         conn->priv.headersToSend = queEntry;
     } else {
@@ -208,8 +217,133 @@ void httpdQueueHeader(HttpdConnData *conn, const char *header, const char *value
         }
         ph->next = queEntry;
     }
+    return true;
 }
 
+
+bool httpdSetCookie(HttpdConnData *conn, const SetCookie *parm)
+{
+    if (!conn || !parm) {
+        return false;
+    }
+    if (!parm->name || !parm->value) {
+        return false;
+    }
+    if (conn->priv.flags & HFL_SENDINGBODY) {
+        http_error("Headers already sent.");
+        return false;
+    }
+
+    /*
+        Set-Cookie: <cookie-name>=<cookie-value>
+        Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>
+        Set-Cookie: <cookie-name>=<cookie-value>; Expires=<date>
+        Set-Cookie: <cookie-name>=<cookie-value>; HttpOnly
+        Set-Cookie: <cookie-name>=<cookie-value>; Max-Age=<number>
+        Set-Cookie: <cookie-name>=<cookie-value>; Partitioned
+        Set-Cookie: <cookie-name>=<cookie-value>; Path=<path-value>
+        Set-Cookie: <cookie-name>=<cookie-value>; Secure
+
+        Set-Cookie: <cookie-name>=<cookie-value>; SameSite=Strict
+        Set-Cookie: <cookie-name>=<cookie-value>; SameSite=Lax
+        Set-Cookie: <cookie-name>=<cookie-value>; SameSite=None; Secure
+
+        // Multiple attributes are also possible, for example:
+        Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly
+     */
+
+#define COOKIE_PART_SET_COOKIE "Set-Cookie: "
+#define COOKIE_PART_EXPIRES "; Expires="
+#define COOKIE_PART_MAX_AGE "; Max-Age="
+#define COOKIE_PART_DOMAIN "; Domain="
+#define COOKIE_PART_PATH "; Path="
+#define COOKIE_PART_SAME_SITE "; SameSite="
+#define COOKIE_PART_SECURE "; Secure"
+#define COOKIE_PART_HTTP_ONLY "; HttpOnly"
+
+    // "Set-Cookie: "
+    size_t buflen = strlen(COOKIE_PART_SET_COOKIE)
+                    + strlen(parm->name) + 1 + strlen(parm->value)
+                    + 5; // cr lf + nul + some spare space
+    if (parm->expires) {
+        buflen += strlen(COOKIE_PART_EXPIRES) + strlen(parm->expires);
+    }
+
+    if (parm->maxAge != 0) {
+        // 10 chars are needed for 32bit integer
+        buflen += strlen(COOKIE_PART_MAX_AGE) + 10;
+    }
+
+    if (parm->domain) {
+        buflen += strlen(COOKIE_PART_DOMAIN) + strlen(parm->domain);
+    }
+
+    if (parm->path) {
+        buflen += strlen(COOKIE_PART_PATH) + strlen(parm->path);
+    }
+
+    if (parm->sameSite) {
+        // Strict, Lax, None
+        buflen += strlen(COOKIE_PART_SAME_SITE) + strlen(parm->sameSite);
+    }
+
+    if (parm->secure) {
+        buflen += strlen(COOKIE_PART_SECURE);
+    }
+
+    if (parm->httponly) {
+        buflen += strlen(COOKIE_PART_HTTP_ONLY);
+    }
+
+    HttpdQueuedHeader *queEntry = httpdMalloc(buflen);
+    if (!queEntry) {
+        http_error("httpdSetCookie - no mem");
+        return false;
+    }
+
+    queEntry->next = NULL;
+    queEntry->headerLine[0] = 0;
+    strcat(queEntry->headerLine, COOKIE_PART_SET_COOKIE);
+    strcat(queEntry->headerLine, parm->name);
+    strcat(queEntry->headerLine, "=");
+    strcat(queEntry->headerLine, parm->value);
+    if (parm->expires) {
+        strcat(queEntry->headerLine, COOKIE_PART_EXPIRES);
+        strcat(queEntry->headerLine, parm->expires);
+    }
+    if (parm->maxAge != 0) {
+        strcat(queEntry->headerLine, COOKIE_PART_MAX_AGE);
+        sprintf(queEntry->headerLine + strlen(queEntry->headerLine), "%d", parm->maxAge);
+    }
+    if (parm->domain) {
+        strcat(queEntry->headerLine, COOKIE_PART_DOMAIN);
+        strcat(queEntry->headerLine, parm->domain);
+    }
+    if (parm->path) {
+        strcat(queEntry->headerLine, COOKIE_PART_PATH);
+        strcat(queEntry->headerLine, parm->path);
+    }
+    if (parm->sameSite) {
+        strcat(queEntry->headerLine, COOKIE_PART_SAME_SITE);
+        strcat(queEntry->headerLine, parm->sameSite);
+    }
+    if (parm->secure) {
+        strcat(queEntry->headerLine, COOKIE_PART_SECURE);
+    }
+    if (parm->httponly) {
+        strcat(queEntry->headerLine, COOKIE_PART_HTTP_ONLY);
+    }
+    strcat(queEntry->headerLine, "\r\n");
+
+    if (!httpdQueueHeaderRaw(conn, queEntry)) {
+        // should not be possible
+        httpdFree(queEntry);
+    }
+
+    return true;
+}
+
+
 void httdSetTransferMode(HttpdConnData *conn, httpd_transfer_opt mode)
 {
     if (mode == HTTPD_TRANSFER_CLOSE) {