diff --git a/hostapd/Makefile b/hostapd/Makefile
index dd3816e0d..325372801 100644
--- a/hostapd/Makefile
+++ b/hostapd/Makefile
@@ -326,6 +326,12 @@ NEED_SHA384=y
 NEED_SHA512=y
 endif
 
+ifdef CONFIG_AIRTIME_POLICY
+CFLAGS += -DCONFIG_AIRTIME_POLICY
+OBJS += ../src/ap/airtime_policy.o
+endif
+
+
 ifdef CONFIG_FILS
 CFLAGS += -DCONFIG_FILS
 OBJS += ../src/ap/fils_hlp.o
diff --git a/hostapd/config_file.c b/hostapd/config_file.c
index aeec1d9e2..cefa90e85 100644
--- a/hostapd/config_file.c
+++ b/hostapd/config_file.c
@@ -2296,6 +2296,42 @@ static unsigned int parse_tls_flags(const char *val)
 }
 #endif /* EAP_SERVER */
 
+#ifdef CONFIG_AIRTIME_POLICY
+static int add_airtime_weight(struct hostapd_bss_config *bss, char *value)
+{
+	struct airtime_sta_weight *wt;
+	char *pos, *next;
+
+	wt = os_zalloc(sizeof(*wt));
+	if (wt == NULL)
+		return -1;
+
+	/* 02:01:02:03:04:05 10 */
+	pos = value;
+	next = os_strchr(pos, ' ');
+	if (next)
+		*next++ = '\0';
+	if (next == NULL || hwaddr_aton(pos, wt->addr)) {
+		wpa_printf(MSG_ERROR, "Invalid station address: '%s'", pos);
+		os_free(wt);
+		return -1;
+	}
+
+	pos = next;
+	wt->weight = atoi(pos);
+	if (!wt->weight) {
+		wpa_printf(MSG_ERROR, "Invalid weight: '%s'", pos);
+		os_free(wt);
+		return -1;
+	}
+
+	wt->next = bss->airtime_weight_list;
+	bss->airtime_weight_list = wt;
+	return 0;
+}
+#endif /* CONFIG_AIRTIME_POLICY */
+
+
 
 #ifdef CONFIG_SAE
 static int parse_sae_password(struct hostapd_bss_config *bss, const char *val)
@@ -4262,6 +4298,22 @@ static int hostapd_config_fill(struct hostapd_config *conf,
 		conf->rssi_reject_assoc_timeout = atoi(pos);
 	} else if (os_strcmp(buf, "pbss") == 0) {
 		bss->pbss = atoi(pos);
+#ifdef CONFIG_AIRTIME_POLICY
+	} else if (os_strcmp(buf, "airtime_mode") == 0) {
+		conf->airtime_mode = atoi(pos);
+	} else if (os_strcmp(buf, "airtime_update_interval") == 0) {
+		conf->airtime_update_interval = atoi(pos);
+	} else if (os_strcmp(buf, "airtime_bss_weight") == 0) {
+		bss->airtime_weight = atoi(pos);
+	} else if (os_strcmp(buf, "airtime_bss_limit") == 0) {
+		bss->airtime_limit = atoi(pos);
+	} else if (os_strcmp(buf, "airtime_sta_weight") == 0) {
+		if (add_airtime_weight(bss, pos) < 0) {
+			wpa_printf(MSG_DEBUG, "Line %d: Invalid airtime weight '%s'",
+				   line, pos);
+			return 1;
+		}
+#endif /* CONFIG_AIRTIME_POLICY */
 	} else {
 		wpa_printf(MSG_ERROR,
 			   "Line %d: unknown configuration item '%s'",
diff --git a/hostapd/defconfig b/hostapd/defconfig
index ea5e2c9de..10c032e82 100644
--- a/hostapd/defconfig
+++ b/hostapd/defconfig
@@ -376,6 +376,9 @@ CONFIG_IPV6=y
 # Experimental implementation of draft-harkins-owe-07.txt
 #CONFIG_OWE=y
 
+# Airtime policy supprot
+#CONFIG_AIRTIME_POLICY=y
+
 # Override default value for the wpa_disable_eapol_key_retries configuration
 # parameter. See that parameter in hostapd.conf for more details.
 #CFLAGS += -DDEFAULT_WPA_DISABLE_EAPOL_KEY_RETRIES=1
diff --git a/hostapd/hostapd.conf b/hostapd/hostapd.conf
index ab37f03b5..ea2471862 100644
--- a/hostapd/hostapd.conf
+++ b/hostapd/hostapd.conf
@@ -2422,6 +2422,42 @@ own_ip_addr=127.0.0.1
 # that allows sending of such data. Default: 0.
 #stationary_ap=0
 
+##### Airtime policy configuration ###########################################
+
+# Set the airtime policy operating mode:
+# 0 = disabled (default)
+# 1 = static config
+# 2 = per-BSS dynamic config
+# 3 = per-BSS limit mode
+#airtime_mode=0
+
+# Interval (in milliseconds) to poll the kernel for updated station activity in
+# dynamic and limit modes
+#airtime_update_interval=200
+
+# Static configuration of station weights (when airtime_mode=1). Kernel default
+# weight is 256; set higher for larger airtime share, lower for smaller share.
+# Each entry is a MAC address followed by a weight.
+#airtime_sta_weight=02:01:02:03:04:05 256
+#airtime_sta_weight=02:01:02:03:04:06 512
+
+# Per-BSS airtime weight. In multi-bss mode, set for each BSS and hostapd will
+# configure station weights to enforce the correct ratio between BSS weights
+# depending on the number of active stations. The *ratios* between different
+# BSSes is what's important, not the absolute numbers.
+# Must be set for all BSSes if airtime_mode=2 or 3, has no effect otherwise.
+#airtime_bss_weight=1
+
+# Whether the current BSS should be limited (when airtime_mode=3).
+#
+# If set, the BSS weight ratio will be applied in the case where the current BSS
+# would exceed the share defined by the BSS weight ratio. E.g., if two BSSes are
+# set to the same weights, and one is set to limited, the limited BSS will get
+# no more than half the available airtime, but if the non-limited BSS has more
+# stations active, that *will* be allowed to exceed its half of the available
+# airtime.
+#airtime_bss_limit=1
+
 ##### TESTING OPTIONS #########################################################
 #
 # The options in this section are only available when the build configuration
diff --git a/src/ap/Makefile b/src/ap/Makefile
index 9b07ee163..7cfd9a48b 100644
--- a/src/ap/Makefile
+++ b/src/ap/Makefile
@@ -20,6 +20,7 @@ CFLAGS += -DCONFIG_WPS
 CFLAGS += -DCONFIG_PROXYARP
 CFLAGS += -DCONFIG_IPV6
 CFLAGS += -DCONFIG_IAPP
+CFLAGS += -DCONFIG_AIRITME_POLICY
 
 LIB_OBJS= \
 	accounting.o \
@@ -27,6 +28,7 @@ LIB_OBJS= \
 	ap_drv_ops.o \
 	ap_list.o \
 	ap_mlme.o \
+	airtime_policy.o \
 	authsrv.o \
 	beacon.o \
 	bss_load.o \
diff --git a/src/ap/airtime_policy.c b/src/ap/airtime_policy.c
new file mode 100644
index 000000000..4ceb0f51c
--- /dev/null
+++ b/src/ap/airtime_policy.c
@@ -0,0 +1,252 @@
+/*
+ * Airtime policy configuration
+ * Copyright (c) 2018-2019, Toke Høiland-Jørgensen <toke@toke.dk>
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#include "utils/includes.h"
+
+#include "utils/common.h"
+#include "utils/eloop.h"
+#include "hostapd.h"
+#include "airtime_policy.h"
+#include "ap_drv_ops.h"
+#include "sta_info.h"
+
+#ifdef CONFIG_AIRTIME_POLICY
+
+/* Idea:
+ * Two modes of airtime enforcement:
+ * 1. Static weights: specify weights per MAC address with a per-BSS default
+ * 2. Per-BSS limits: Dynamically calculate weights of backlogged stations to
+ *    enforce relative total shares between BSSes.
+ *
+ * - Periodic per-station callback to update queue status.
+ *
+ * Copy accounting_sta_update_stats() to get TXQ info and airtime weights and
+ * keep them updated in sta_info
+ *
+ * - Separate periodic per-bss (or per-iface?) callback to update weights.
+ *
+ * Just need to loop through all interfaces, count sum the active stations (or
+ * should the per-sta callback just adjust that for the bss?) and calculate new
+ * weights.
+ */
+
+static int get_airtime_policy_update_timeout(struct hostapd_iface *iface,
+					unsigned int *sec, unsigned int *usec)
+{
+	unsigned int update_int = iface->conf->airtime_update_interval;
+
+	if (!update_int) {
+		wpa_printf(MSG_ERROR,
+			   "Airtime policy: Invalid airtime policy update interval %u",
+			   update_int);
+		return -1;
+	}
+
+	*sec = update_int / 1000;
+	*usec = (update_int % 1000) * 1000;
+
+	return 0;
+}
+
+static inline void set_new_backlog_time(struct hostapd_data *hapd, struct sta_info *sta,
+					struct os_reltime *now)
+{
+	sta->backlogged_until = *now;
+	sta->backlogged_until.usec += hapd->iconf->airtime_update_interval * AIRTIME_BACKLOG_EXPIRY_FACTOR;
+	while (sta->backlogged_until.usec >= 1000000) {
+		sta->backlogged_until.sec++;
+		sta->backlogged_until.usec -= 1000000;
+	}
+}
+
+static void count_backlogged_sta(struct hostapd_data *hapd)
+{
+	struct sta_info *sta;
+	struct hostap_sta_driver_data data = {};
+	unsigned int num_backlogged = 0;
+	struct os_reltime now;
+
+	os_get_reltime(&now);
+
+	for (sta = hapd->sta_list; sta; sta = sta->next) {
+		if (hostapd_drv_read_sta_data(hapd, &data, sta->addr))
+			continue;
+
+		if (data.backlog_bytes > 0)
+			set_new_backlog_time(hapd, sta, &now);
+		if (os_reltime_before(&now, &sta->backlogged_until))
+			num_backlogged++;
+	}
+	hapd->num_backlogged_sta = num_backlogged;
+}
+
+static int sta_set_airtime_weight(struct hostapd_data *hapd, struct sta_info *sta,
+				  unsigned int weight)
+{
+	int ret = 0;
+
+	if (weight != sta->airtime_weight &&
+	    (ret = hostapd_sta_set_airtime_weight(hapd, sta->addr, weight)))
+		return ret;
+
+	sta->airtime_weight = weight;
+	return ret;
+}
+
+static void set_sta_weights(struct hostapd_data *hapd, unsigned int weight)
+{
+	struct sta_info *sta;
+	for (sta = hapd->sta_list; sta; sta = sta->next)
+		sta_set_airtime_weight(hapd, sta, weight);
+}
+
+static unsigned int get_airtime_quantum(unsigned int max_wt)
+{
+	unsigned int quantum = AIRTIME_QUANTUM_TARGET / max_wt;
+
+	if (quantum < AIRTIME_QUANTUM_MIN)
+		quantum = AIRTIME_QUANTUM_MIN;
+	else if (quantum > AIRTIME_QUANTUM_MAX)
+		quantum = AIRTIME_QUANTUM_MAX;
+
+	return quantum;
+}
+
+
+static void update_airtime_weights(void *eloop_data, void *user_data)
+{
+	struct hostapd_iface *iface = eloop_data;
+	struct hostapd_data *bss;
+	unsigned int sec, usec;
+	unsigned int num_sta_min = 0, num_sta_prod = 1, num_sta_sum = 0, wt_sum = 0;
+	unsigned int quantum;
+	Boolean all_div_min = TRUE, apply_limit = (iface->conf->airtime_mode == AIRTIME_MODE_DYNAMIC);
+	int i, wt, num_bss = 0, max_wt = 0;
+
+	for (i = 0; i < iface->num_bss; i++) {
+		bss = iface->bss[i];
+		if (!bss->started || !bss->conf->airtime_weight)
+			continue;
+
+		count_backlogged_sta(bss);
+		if (!bss->num_backlogged_sta)
+			continue;
+
+		if (!num_sta_min || bss->num_backlogged_sta < num_sta_min)
+			num_sta_min = bss->num_backlogged_sta;
+
+		num_sta_prod *= bss->num_backlogged_sta;
+		num_sta_sum += bss->num_backlogged_sta;
+		wt_sum += bss->conf->airtime_weight;
+		num_bss++;
+	}
+
+	if (num_sta_min) {
+		for (i = 0; i < iface->num_bss; i++) {
+			bss = iface->bss[i];
+			if (!bss->started || !bss->conf->airtime_weight)
+				continue;
+
+			/* Check if we can divide all sta numbers by the smallest number
+			 * to keep weights as small as possible. This is a lazy way to
+			 * avoid having to factor integers. */
+			if (bss->num_backlogged_sta && bss->num_backlogged_sta % num_sta_min > 0)
+				all_div_min = FALSE;
+
+			/* If we're in LIMIT mode, we only apply the weight scaling when
+			 * the BSS(es) marked as limited would a larger share than the relative
+			 * BSS weights indicates it should. */
+			if (!apply_limit && bss->conf->airtime_limit) {
+				if (bss->num_backlogged_sta * wt_sum > bss->conf->airtime_weight * num_sta_sum)
+					apply_limit = TRUE;
+			}
+		}
+		if (all_div_min)
+			num_sta_prod /= num_sta_min;
+	}
+
+	for (i = 0; i < iface->num_bss; i++) {
+		bss = iface->bss[i];
+		if (!bss->started  || !bss->conf->airtime_weight)
+			continue;
+
+		/* We only set the calculated weight if the BSS has active
+		 * stations and there are other active interfaces as well -
+		 * otherwise we just set a unit weight. This ensures that
+		 * the weights are set reasonably when stations transition from
+		 * inactive to active. */
+		if (apply_limit && bss->num_backlogged_sta && num_bss > 1)
+			wt = bss->conf->airtime_weight * num_sta_prod / bss->num_backlogged_sta;
+		else
+			wt = 1;
+
+		bss->airtime_weight = wt;
+		if (wt > max_wt)
+			max_wt = wt;
+	}
+
+	quantum = get_airtime_quantum(max_wt);
+
+	for (i = 0; i < iface->num_bss; i++) {
+		bss = iface->bss[i];
+		if (!bss->started  || !bss->conf->airtime_weight)
+			continue;
+		set_sta_weights(bss, bss->airtime_weight * quantum);
+	}
+
+	if (get_airtime_policy_update_timeout(iface, &sec, &usec) < 0)
+		return;
+
+	eloop_register_timeout(sec, usec, update_airtime_weights, iface,
+			       NULL);
+}
+
+static int get_weight_for_sta(struct hostapd_data *hapd, const u8 *sta)
+{
+	struct airtime_sta_weight *wt;
+
+	wt = hapd->conf->airtime_weight_list;
+	while (wt != NULL && os_memcmp(wt->addr, sta, 6) != 0)
+		wt = wt->next;
+
+	return wt ? wt->weight : hapd->conf->airtime_weight;
+}
+
+int airtime_policy_new_sta(struct hostapd_data *hapd, struct sta_info *sta)
+{
+	int weight;
+
+	if (hapd->iconf->airtime_mode == AIRTIME_MODE_STATIC) {
+		weight = get_weight_for_sta(hapd, sta->addr);
+		if (weight)
+			return sta_set_airtime_weight(hapd, sta, weight);
+	}
+	return 0;
+}
+
+int airtime_policy_update_init(struct hostapd_iface *iface)
+{
+	unsigned int sec, usec;
+
+	if (iface->conf->airtime_mode < AIRTIME_MODE_DYNAMIC)
+		return 0;
+
+	if (get_airtime_policy_update_timeout(iface, &sec, &usec) < 0)
+		return -1;
+
+	eloop_register_timeout(sec, usec, update_airtime_weights, iface,
+			       NULL);
+	return 0;
+}
+
+void airtime_policy_update_deinit(struct hostapd_iface *iface)
+{
+	eloop_cancel_timeout(update_airtime_weights, iface, NULL);
+}
+
+#endif /* CONFIG_AIRTIME_POLICY */
diff --git a/src/ap/airtime_policy.h b/src/ap/airtime_policy.h
new file mode 100644
index 000000000..483d10928
--- /dev/null
+++ b/src/ap/airtime_policy.h
@@ -0,0 +1,31 @@
+/*
+ * Airtime policy configuration
+ * Copyright (c) 2018-2019, Toke Høiland-Jørgensen <toke@toke.dk>
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#ifndef AIRTIME_POLICY_LOAD_H
+#define AIRTIME_POLICY_LOAD_H
+
+#include "hostapd.h"
+
+#ifdef CONFIG_AIRTIME_POLICY
+
+#define AIRTIME_DEFAULT_UPDATE_INTERVAL 200 /* ms */
+#define AIRTIME_BACKLOG_EXPIRY_FACTOR 2500 /* 2.5 intervals + convert to usec */
+
+/* scale quantum so this becomes the effective quantum after applying the max
+ * weight, but never go below min or above max */
+#define AIRTIME_QUANTUM_MIN 8 /* usec */
+#define AIRTIME_QUANTUM_MAX 256 /* usec */
+#define AIRTIME_QUANTUM_TARGET 1024 /* usec */
+
+int airtime_policy_new_sta(struct hostapd_data *hapd, struct sta_info *sta);
+int airtime_policy_update_init(struct hostapd_iface *iface);
+void airtime_policy_update_deinit(struct hostapd_iface *iface);
+
+#endif /* CONFIG_AIRTIME_POLICY */
+
+#endif /* AIRTIME_POLICY_LOAD_H */
diff --git a/src/ap/ap_config.c b/src/ap/ap_config.c
index 9611dc0b3..4b004b3d7 100644
--- a/src/ap/ap_config.c
+++ b/src/ap/ap_config.c
@@ -19,6 +19,7 @@
 #include "eap_server/eap.h"
 #include "wpa_auth.h"
 #include "sta_info.h"
+#include "airtime_policy.h"
 #include "ap_config.h"
 
 
@@ -244,6 +245,10 @@ struct hostapd_config * hostapd_config_defaults(void)
 	conf->rssi_reject_assoc_rssi = 0;
 	conf->rssi_reject_assoc_timeout = 30;
 
+#ifdef CONFIG_AIRTIME_POLICY
+	conf->airtime_update_interval = AIRTIME_DEFAULT_UPDATE_INTERVAL;
+#endif
+
 	return conf;
 }
 
@@ -734,6 +739,20 @@ void hostapd_config_free_bss(struct hostapd_bss_config *conf)
 
 	hostapd_config_free_sae_passwords(conf);
 
+#ifdef CONFIG_AIRTIME_POLICY
+	{
+		struct airtime_sta_weight *wt, *wt_prev;
+
+		wt = conf->airtime_weight_list;
+		conf->airtime_weight_list = NULL;
+		while (wt) {
+			wt_prev = wt;
+			wt = wt->next;
+			os_free(wt_prev);
+		}
+	}
+#endif /* CONFIG_AIRTIME_POLICY */
+
 	os_free(conf);
 }
 
@@ -1068,6 +1087,18 @@ static int hostapd_config_check_bss(struct hostapd_bss_config *bss,
 	}
 #endif /* CONFIG_OCV */
 
+#ifdef CONFIG_AIRTIME_POLICY
+	if (full_config && bss->airtime_weight < 0) {
+		wpa_printf(MSG_ERROR, "Airtime BSS weight must be >= 0");
+		return -1;
+	}
+
+	if (full_config && (bss->airtime_limit < 0 || bss->airtime_limit > 1)) {
+		wpa_printf(MSG_ERROR, "Airtime BSS limit must be 0 or 1");
+		return -1;
+	}
+#endif /* CONFIG_AIRTIME_POLICY */
+
 	return 0;
 }
 
@@ -1124,6 +1155,17 @@ int hostapd_config_check(struct hostapd_config *conf, int full_config)
 		return -1;
 	}
 
+#ifdef CONFIG_AIRTIME_POLICY
+	if (full_config && conf->airtime_mode > AIRTIME_MODE_MAX) {
+		wpa_printf(MSG_ERROR, "Unknown airtime mode");
+		return -1;
+	}
+
+	if (full_config && conf->airtime_mode > AIRTIME_MODE_STATIC && !conf->airtime_update_interval) {
+		wpa_printf(MSG_ERROR, "Airtime update interval cannot be zero");
+		return -1;
+	}
+#endif /* CONFIG_AIRTIME_POLICY */
 	for (i = 0; i < NUM_TX_QUEUES; i++) {
 		if (hostapd_config_check_cw(conf, i))
 			return -1;
diff --git a/src/ap/ap_config.h b/src/ap/ap_config.h
index 6963df460..1ed53c9a0 100644
--- a/src/ap/ap_config.h
+++ b/src/ap/ap_config.h
@@ -250,6 +250,12 @@ struct sae_password_entry {
 	u8 peer_addr[ETH_ALEN];
 };
 
+struct airtime_sta_weight {
+	struct airtime_sta_weight *next;
+	int weight;
+	u8 addr[6];
+};
+
 /**
  * struct hostapd_bss_config - Per-BSS configuration
  */
@@ -704,6 +710,12 @@ struct hostapd_bss_config {
 #define BACKHAUL_BSS 1
 #define FRONTHAUL_BSS 2
 	int multi_ap; /* bitmap of BACKHAUL_BSS, FRONTHAUL_BSS */
+
+#ifdef CONFIG_AIRTIME_POLICY
+	int airtime_weight;
+	int airtime_limit;
+	struct airtime_sta_weight *airtime_weight_list;
+#endif /* CONFIG_AIRTIME_POLICY */
 };
 
 /**
@@ -856,6 +868,18 @@ struct hostapd_config {
 
 	int rssi_reject_assoc_rssi;
 	int rssi_reject_assoc_timeout;
+
+#ifdef CONFIG_AIRTIME_POLICY
+	enum {
+		AIRTIME_MODE_OFF = 0,
+		AIRTIME_MODE_STATIC = 1,
+		AIRTIME_MODE_DYNAMIC = 2,
+		AIRTIME_MODE_LIMIT = 3,
+		__AIRTIME_MODE_MAX,
+	} airtime_mode;
+	unsigned int airtime_update_interval;
+#define AIRTIME_MODE_MAX ( __AIRTIME_MODE_MAX -1)
+#endif /* CONFIG_AIRTIME_POLICY */
 };
 
 
diff --git a/src/ap/ap_drv_ops.c b/src/ap/ap_drv_ops.c
index 067cf863e..cfc55952a 100644
--- a/src/ap/ap_drv_ops.c
+++ b/src/ap/ap_drv_ops.c
@@ -582,6 +582,14 @@ int hostapd_sta_set_flags(struct hostapd_data *hapd, u8 *addr,
 					   flags_or, flags_and);
 }
 
+int hostapd_sta_set_airtime_weight(struct hostapd_data *hapd, u8 *addr,
+				   unsigned int weight)
+{
+	if (hapd->driver == NULL || hapd->driver->sta_set_airtime_weight == NULL)
+		return 0;
+	return hapd->driver->sta_set_airtime_weight(hapd->drv_priv, addr, weight);
+}
+
 
 int hostapd_set_country(struct hostapd_data *hapd, const char *country)
 {
diff --git a/src/ap/ap_drv_ops.h b/src/ap/ap_drv_ops.h
index d45ab8462..a40855d96 100644
--- a/src/ap/ap_drv_ops.h
+++ b/src/ap/ap_drv_ops.h
@@ -67,6 +67,8 @@ int hostapd_set_rts(struct hostapd_data *hapd, int rts);
 int hostapd_set_frag(struct hostapd_data *hapd, int frag);
 int hostapd_sta_set_flags(struct hostapd_data *hapd, u8 *addr,
 			  int total_flags, int flags_or, int flags_and);
+int hostapd_sta_set_airtime_weight(struct hostapd_data *hapd, u8 *addr,
+				   unsigned int weight);
 int hostapd_set_country(struct hostapd_data *hapd, const char *country);
 int hostapd_set_tx_queue_params(struct hostapd_data *hapd, int queue, int aifs,
 				int cw_min, int cw_max, int burst_time);
diff --git a/src/ap/hostapd.c b/src/ap/hostapd.c
index 342585f92..b8db488da 100644
--- a/src/ap/hostapd.c
+++ b/src/ap/hostapd.c
@@ -50,6 +50,7 @@
 #include "fils_hlp.h"
 #include "acs.h"
 #include "hs20.h"
+#include "airtime_policy.h"
 
 
 static int hostapd_flush_old_stations(struct hostapd_data *hapd, u16 reason);
@@ -476,6 +477,9 @@ static void hostapd_cleanup_iface_partial(struct hostapd_iface *iface)
 	iface->basic_rates = NULL;
 	ap_list_deinit(iface);
 	sta_track_deinit(iface);
+#ifdef CONFIG_AIRTIME_POLICY
+	airtime_policy_update_deinit(iface);
+#endif
 }
 
 
@@ -1954,6 +1958,9 @@ dfs_offload:
 
 	hostapd_set_state(iface, HAPD_IFACE_ENABLED);
 	hostapd_owe_update_trans(iface);
+#ifdef CONFIG_AIRTIME_POLICY
+	airtime_policy_update_init(iface);
+#endif
 	wpa_msg(iface->bss[0]->msg_ctx, MSG_INFO, AP_EVENT_ENABLED);
 	if (hapd->setup_complete_cb)
 		hapd->setup_complete_cb(hapd->setup_complete_cb_ctx);
@@ -2966,6 +2973,9 @@ void hostapd_new_assoc_sta(struct hostapd_data *hapd, struct sta_info *sta,
 			hostapd_p2p_non_p2p_sta_connected(hapd);
 	}
 #endif /* CONFIG_P2P */
+#ifdef CONFIG_AIRTIME_POLICY
+	airtime_policy_new_sta(hapd, sta);
+#endif /* CONFIG_AIRTIME_POLICY */
 
 	/* Start accounting here, if IEEE 802.1X and WPA are not used.
 	 * IEEE 802.1X/WPA code will start accounting after the station has
diff --git a/src/ap/hostapd.h b/src/ap/hostapd.h
index d304c1171..c92983c51 100644
--- a/src/ap/hostapd.h
+++ b/src/ap/hostapd.h
@@ -371,6 +371,11 @@ struct hostapd_data {
 	unsigned int dpp_ignore_netaccesskey_mismatch:1;
 #endif /* CONFIG_TESTING_OPTIONS */
 #endif /* CONFIG_DPP */
+
+#ifdef CONFIG_AIRTIME_POLICY
+	unsigned int num_backlogged_sta;
+	unsigned int airtime_weight;
+#endif /* CONFIG_AIRTIME_POLICY */
 };
 
 
@@ -533,6 +538,9 @@ struct hostapd_iface {
 	unsigned int num_sta_seen;
 
 	u8 dfs_domain;
+#ifdef CONFIG_AIRTIME_POLICY
+	unsigned int airtime_quantum;
+#endif /* CONFIG_AIRTIME_POLICY */
 };
 
 /* hostapd.c */
diff --git a/src/ap/sta_info.h b/src/ap/sta_info.h
index ee3f628a4..1e7706811 100644
--- a/src/ap/sta_info.h
+++ b/src/ap/sta_info.h
@@ -271,6 +271,10 @@ struct sta_info {
 	u8 last_tk[WPA_TK_MAX_LEN];
 	size_t last_tk_len;
 #endif /* CONFIG_TESTING_OPTIONS */
+#ifdef CONFIG_AIRTIME_POLICY
+	int airtime_weight;
+	struct os_reltime backlogged_until;
+#endif
 };
 
 
diff --git a/src/drivers/driver.h b/src/drivers/driver.h
index 23423d92e..eda22f89b 100644
--- a/src/drivers/driver.h
+++ b/src/drivers/driver.h
@@ -1752,6 +1752,7 @@ struct hostapd_data;
 struct hostap_sta_driver_data {
 	unsigned long rx_packets, tx_packets;
 	unsigned long long rx_bytes, tx_bytes;
+	unsigned long long rx_airtime, tx_airtime;
 	int bytes_64bit; /* whether 64-bit byte counters are supported */
 	unsigned long current_tx_rate;
 	unsigned long current_rx_rate;
@@ -1761,6 +1762,8 @@ struct hostap_sta_driver_data {
 	unsigned long tx_retry_failed;
 	unsigned long tx_retry_count;
 	s8 last_ack_rssi;
+	unsigned long backlog_packets;
+	unsigned long backlog_bytes;
 	s8 signal;
 	u8 rx_vhtmcs;
 	u8 tx_vhtmcs;
@@ -2935,6 +2938,16 @@ struct wpa_driver_ops {
 			     unsigned int total_flags, unsigned int flags_or,
 			     unsigned int flags_and);
 
+	/**
+	 * sta_set_airtime_weight - Set station airtime weight (AP only)
+	 * @priv: Private driver interface data
+	 * @addr: Station address
+	 * @weight: New weight for station airtime assignment
+	 * Returns: 0 on success, -1 on failure
+	 */
+	int (*sta_set_airtime_weight)(void *priv, const u8 *addr,
+				unsigned int weight);
+
 	/**
 	 * set_tx_queue_params - Set TX queue parameters
 	 * @priv: Private driver interface data
diff --git a/src/drivers/driver_nl80211.c b/src/drivers/driver_nl80211.c
index e0e6fe52c..f4ab3b6cd 100644
--- a/src/drivers/driver_nl80211.c
+++ b/src/drivers/driver_nl80211.c
@@ -5169,6 +5169,27 @@ fail:
 	return -ENOBUFS;
 }
 
+static int driver_nl80211_sta_set_airtime_weight(void *priv, const u8 *addr,
+						 unsigned int weight)
+{
+	struct i802_bss *bss = priv;
+	struct nl_msg *msg;
+	struct nlattr *flags;
+
+	wpa_printf(MSG_DEBUG, "nl80211: Set STA airtime weight - ifname=%s addr=" MACSTR
+		   " weight=%u", bss->ifname, MAC2STR(addr), weight);
+
+	if (!(msg = nl80211_bss_msg(bss, 0, NL80211_CMD_SET_STATION)) ||
+	    nla_put(msg, NL80211_ATTR_MAC, ETH_ALEN, addr) ||
+	    nla_put_u16(msg, NL80211_ATTR_AIRTIME_WEIGHT, weight))
+		goto fail;
+
+	return send_and_recv_msgs(bss->drv, msg, NULL, NULL);
+fail:
+	nlmsg_free(msg);
+	return -ENOBUFS;
+}
+
 
 static int wpa_driver_nl80211_ap(struct wpa_driver_nl80211_data *drv,
 				 struct wpa_driver_associate_params *params)
@@ -6253,6 +6274,8 @@ static int get_sta_handler(struct nl_msg *msg, void *arg)
 		[NL80211_STA_INFO_TX_BYTES64] = { .type = NLA_U64 },
 		[NL80211_STA_INFO_SIGNAL] = { .type = NLA_U8 },
 		[NL80211_STA_INFO_ACK_SIGNAL] = { .type = NLA_U8 },
+		[NL80211_STA_INFO_RX_DURATION] = { .type = NLA_U64 },
+		[NL80211_STA_INFO_TX_DURATION] = { .type = NLA_U64 },
 	};
 	struct nlattr *rate[NL80211_RATE_INFO_MAX + 1];
 	static struct nla_policy rate_policy[NL80211_RATE_INFO_MAX + 1] = {
@@ -6263,6 +6286,13 @@ static int get_sta_handler(struct nl_msg *msg, void *arg)
 		[NL80211_RATE_INFO_SHORT_GI] = { .type = NLA_FLAG },
 		[NL80211_RATE_INFO_VHT_NSS] = { .type = NLA_U8 },
 	};
+	struct nlattr *tid_stats[NL80211_TID_STATS_MAX + 1], *tidattr;
+	struct nlattr *txq_stats[NL80211_TXQ_STATS_MAX + 1];
+	static struct nla_policy txq_stats_policy[NL80211_TXQ_STATS_MAX +1] = {
+		[NL80211_TXQ_STATS_BACKLOG_BYTES] = { .type = NLA_U32 },
+		[NL80211_TXQ_STATS_BACKLOG_PACKETS] = { .type = NLA_U32 },
+	};
+	int rem;
 
 	nla_parse(tb, NL80211_ATTR_MAX, genlmsg_attrdata(gnlh, 0),
 		  genlmsg_attrlen(gnlh, 0), NULL);
@@ -6310,6 +6340,10 @@ static int get_sta_handler(struct nl_msg *msg, void *arg)
 	if (stats[NL80211_STA_INFO_TX_PACKETS])
 		data->tx_packets =
 			nla_get_u32(stats[NL80211_STA_INFO_TX_PACKETS]);
+	if (stats[NL80211_STA_INFO_RX_DURATION])
+		data->rx_airtime = nla_get_u64(stats[NL80211_STA_INFO_RX_DURATION]);
+	if (stats[NL80211_STA_INFO_TX_DURATION])
+		data->tx_airtime = nla_get_u64(stats[NL80211_STA_INFO_TX_DURATION]);
 	if (stats[NL80211_STA_INFO_TX_FAILED])
 		data->tx_retry_failed =
 			nla_get_u32(stats[NL80211_STA_INFO_TX_FAILED]);
@@ -6380,6 +6414,27 @@ static int get_sta_handler(struct nl_msg *msg, void *arg)
 		}
 	}
 
+	if (stats[NL80211_STA_INFO_TID_STATS]) {
+		nla_for_each_nested(tidattr, stats[NL80211_STA_INFO_TID_STATS], rem) {
+			if (nla_parse_nested(tid_stats, NL80211_TID_STATS_MAX,
+					     tidattr, NULL) == 0 &&
+			    tid_stats[NL80211_TID_STATS_TXQ_STATS] &&
+			    nla_parse_nested(txq_stats, NL80211_TXQ_STATS_MAX,
+					     tid_stats[NL80211_TID_STATS_TXQ_STATS],
+					     txq_stats_policy) == 0) {
+
+				/* sum the backlogs over all TIDs for station */
+				if (txq_stats[NL80211_TXQ_STATS_BACKLOG_BYTES])
+					data->backlog_bytes += nla_get_u32(
+						txq_stats[NL80211_TXQ_STATS_BACKLOG_BYTES]);
+				if (txq_stats[NL80211_TXQ_STATS_BACKLOG_PACKETS])
+					data->backlog_bytes += nla_get_u32(
+						txq_stats[NL80211_TXQ_STATS_BACKLOG_PACKETS]);
+			}
+		}
+	}
+
+
 	return NL_SKIP;
 }
 
@@ -10877,6 +10932,7 @@ const struct wpa_driver_ops wpa_driver_nl80211_ops = {
 	.sta_remove = driver_nl80211_sta_remove,
 	.hapd_send_eapol = wpa_driver_nl80211_hapd_send_eapol,
 	.sta_set_flags = wpa_driver_nl80211_sta_set_flags,
+	.sta_set_airtime_weight = driver_nl80211_sta_set_airtime_weight,
 	.hapd_init = i802_init,
 	.hapd_deinit = i802_deinit,
 	.set_wds_sta = i802_set_wds_sta,
