diff --git a/src/sxwm.c b/src/sxwm.c
index 354c865..749ad65 100644
--- a/src/sxwm.c
+++ b/src/sxwm.c
@@ -30,6 +30,7 @@
 #include <X11/Xutil.h>
 
 #include <X11/extensions/Xinerama.h>
+#include <X11/extensions/Xrandr.h>
 #include <X11/Xcursor/Xcursor.h>
 
 #include "defs.h"
@@ -72,6 +73,7 @@ void hdl_unmap_ntf(XEvent *xev);
 /* void inc_gaps(void); */
 void init_defaults(void);
 Bool is_child_proc(pid_t pid1, pid_t pid2);
+void load_cursors(void);
 /* void move_master_next(void); */
 /* void move_master_prev(void); */
 /* void move_next_mon(void); */
@@ -1035,6 +1037,7 @@ void hdl_config_ntf(XEvent *xev)
 {
 	if (xev->xconfigure.window == root) {
 		update_mons();
+		load_cursors();
 		tile();
 		update_borders();
 	}
@@ -2250,10 +2253,7 @@ void setup(void)
 	grab_keys();
 	startup_exec();
 
-	cursor_normal = XcursorLibraryLoadCursor(dpy, "left_ptr");
-	cursor_move = XcursorLibraryLoadCursor(dpy, "fleur");
-	cursor_resize = XcursorLibraryLoadCursor(dpy, "bottom_right_corner");
-	XDefineCursor(dpy, root, cursor_normal);
+	load_cursors();
 
 	scr_width = XDisplayWidth(dpy, DefaultScreen(dpy));
 	scr_height = XDisplayHeight(dpy, DefaultScreen(dpy));
@@ -3055,6 +3055,60 @@ void update_modifier_masks(void)
 	XFreeModifiermap(mod_mapping);
 }
 
+/* (re)loads the pointer cursors, sizing them from the *primary* monitor's
+ * real DPI instead of Xcursor's own default. Xinerama has no notion of
+ * physical monitor dimensions, so XcursorLibraryLoadCursor falls back to
+ * deriving DPI from the whole virtual screen's pixel height against
+ * whatever mm-height DisplayHeightMM() happens to report for it -- once a
+ * second monitor is added that no longer corresponds to any single
+ * monitor's real DPI, and the cursor renders far too large. Query the
+ * primary monitor's own size via RandR (which does expose mm dimensions)
+ * and set XCURSOR_SIZE from that instead, unless the user already set
+ * XCURSOR_SIZE themselves. Called once at startup and again from
+ * hdl_config_ntf() on every root ConfigureNotify (monitor hotplug/layout
+ * change), so docking/undocking keeps the cursor correctly sized. */
+void load_cursors(void)
+{
+	if (!getenv("XCURSOR_SIZE")) {
+		int rr_event_base, rr_err_base;
+		if (XRRQueryExtension(dpy, &rr_event_base, &rr_err_base)) {
+			int n_info;
+			XRRMonitorInfo *info = XRRGetMonitors(dpy, root, True, &n_info);
+			if (info) {
+				int idx = 0;
+				for (int i = 0; i < n_info; i++) {
+					if (info[i].primary) {
+						idx = i;
+						break;
+					}
+				}
+				if (n_info > 0 && info[idx].mheight > 0 && info[idx].height > 0) {
+					/* cursor_size = dpi * 16 / 96;
+					 * dpi = height_px * 25.4 / mheight_mm */
+					int size = info[idx].height * 16 * 254 / (info[idx].mheight * 960);
+					size = MAX(16, (size + 7) & ~7); /* round up to a multiple of 8 */
+					char buf[16];
+					snprintf(buf, sizeof buf, "%d", size);
+					setenv("XCURSOR_SIZE", buf, 1);
+				}
+				XRRFreeMonitors(info);
+			}
+		}
+	}
+
+	if (cursor_normal)
+		XFreeCursor(dpy, cursor_normal);
+	if (cursor_move)
+		XFreeCursor(dpy, cursor_move);
+	if (cursor_resize)
+		XFreeCursor(dpy, cursor_resize);
+
+	cursor_normal = XcursorLibraryLoadCursor(dpy, "left_ptr");
+	cursor_move = XcursorLibraryLoadCursor(dpy, "fleur");
+	cursor_resize = XcursorLibraryLoadCursor(dpy, "bottom_right_corner");
+	XDefineCursor(dpy, root, cursor_normal);
+}
+
 void update_mons(void)
 {
 	XineramaScreenInfo *info;
diff --git a/Makefile b/Makefile
index 2653a39..46afb75 100644
--- a/Makefile
+++ b/Makefile
@@ -6,7 +6,7 @@ PREFIX = /usr/local
 MANPREFIX = ${PREFIX}/share/man
 
 # libs
-LIBS = -lX11 -lXinerama -lXcursor
+LIBS = -lX11 -lXinerama -lXrandr -lXcursor
 
 # flags
 CPPFLAGS = -D_DEFAULT_SOURCE -D_XOPEN_SOURCE=700
