The error arrived three weeks after the table did, which is what makes it dangerous. A new events table went in with forty-some columns — a handful of VARCHAR(500), a TEXT for a JSON payload, the usual audit timestamps — created cleanly, tested cleanly, deployed cleanly. Then production traffic hit a class of events with unusually large payloads, and INSERTs began failing with ERROR 1118 (42000): Row size too large (> 8126). Changing some columns to TEXT or BLOB may help. Not all inserts. Just the big ones, in bursts, an intermittent failure that looked like an application bug until you counted bytes. The table was in COMPACT row format — an inherited default from an old 5.6-era config that nobody had questioned — and COMPACT keeps the first 768 bytes of every long column on the row's page. Enough long columns, and the on-page part of the row alone blows past half an InnoDB page. The error message names 8126, and understanding that number — why it exists, what counts toward it, and how row formats move bytes off the page — is the difference between a fix that holds and a whack-a-mole of column retypes.
Where does the 8126 limit actually come from?
InnoDB stores rows in 16KB pages (the default innodb_page_size), and its B-tree needs every page to hold at least two rows, or the tree degenerates into a linked list. So the server enforces a hard rule: the on-page portion of any single row must fit in slightly less than half a page — about 8126 bytes on a 16KB page, once page overhead and per-row record overhead are accounted for. Two words in that sentence carry all the operational meaning. First, on-page: variable-length columns can live off-page in separate overflow pages, with only a pointer left in the row, so a 100KB TEXT value does not blow the limit by itself — what counts is the bytes the row format decides to keep on the page. Second, enforce: this is checked at INSERT and UPDATE time against the actual stored width of the row, not the declared width of the columns. That is why the failure is intermittent and data-dependent: the table design is a loaded gun, but only rows with enough long values in the wrong columns actually pull the trigger. It is also why the limit shows up in such strange places — I have seen it fire inside a GROUP BY writing to an internal on-disk temp table, where the "table" is one MySQL invented mid-query, which the internal temp tables notes cover from the spill side.
Why is there a second 1118 with a completely different number?
Because MySQL enforces two unrelated row limits and reports both as ERROR 1118, and confusing them sends you fixing the wrong thing. The create-time limit is 65,535 bytes of declared width across the row, counting non-BLOB/TEXT columns at their maximum possible width — in a utf8mb4 world, that is 4 bytes per character, so a VARCHAR(255) declares 1,020 bytes whether or not any row ever uses them. Sixty-five wide VARCHAR columns and CREATE TABLE itself fails with Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535. That one is a design-time error, fully deterministic, and you meet it in a migration script, not in production. The runtime limit is the 8126 half-page rule above: the create succeeds because InnoDB cannot know how wide your real rows will be, and the error surfaces only when a fat row arrives. The practical consequence: a schema that passes review can still be a production incident three weeks later. When you see 1118, read the number in the message before reaching for a fix — 65535 means shrink declared widths, 8126 means get bytes off the page or off the row.
How do row formats decide what stays on the page?
The row format is the storage layout policy, and the four InnoDB formats differ in exactly one thing that matters here: how aggressively they move long column values to overflow pages. COMPACT and the ancient REDUNDANT store the first 768 bytes of each variable-length column on-page and overflow the rest, so ten columns of 800 bytes each means roughly 8KB on-page before fixed columns are counted — my events table, to the byte. DYNAMIC and COMPRESSED store a 20-byte pointer on-page and push the entire value to overflow pages whenever the row would not otherwise fit, so those same ten columns contribute about 200 bytes of pointers. That is the whole mechanism behind the standard fix:
-- what format is the table actually in?
SELECT TABLE_SCHEMA, TABLE_NAME, ROW_FORMAT
FROM information_schema.TABLES
WHERE TABLE_NAME = 'events';
-- the fix for the 8126 flavor: rebuild into DYNAMIC
-- (8.0's default; a copy operation, so plan the window)
ALTER TABLE app.events ROW_FORMAT=DYNAMIC;
-- check the server default so the next table doesn't inherit the problem
SELECT @@innodb_default_row_format; -- should say 'dynamic'
-- and the per-page physics behind the whole thing
SELECT @@innodb_page_size; -- 16384 on most installs
COMPRESSED is DYNAMIC plus page-level compression of both the row and its overflow pages, with CPU cost and the occasional pathological space amplification when data does not compress — it is a different tool for a different problem, adjacent to but distinct from the page-compression tradeoffs in the compression field notes, and it is rarely the right first answer to 1118. Since MySQL 5.7.9, DYNAMIC has been the default via innodb_default_row_format, so any instance still producing COMPACT tables is carrying an old config value forward — check it before you fix tables one at a time, or the next migration re-creates the bug.
What actually fixes the problem when row format is not enough?
Switching to DYNAMIC resolves the on-page arithmetic for genuinely variable rows, but it does not repeal physics: extremely wide rows still pay for their overflow pages in extra I/O, and some schemas are wide because they are wrong. The fixes that hold, in the order I reach for them. Retype what is lying: VARCHAR(500) columns holding twenty-character codes, CHAR(36) UUIDs that could be BINARY(16) at a third of the bytes, TEXT columns declared for payloads that fit in VARCHAR(1000) — every one of these shrinks either declared width toward the 65535 limit or stored width toward the 8126 one. Move cold payload columns sideways: the 100KB JSON blob that 95 percent of queries never read belongs in a sibling table joined by primary key, so the hot row stays narrow and cache-dense; vertical partitioning is unfashionable and works. Store the payload as actual JSON or TEXT rather than an overstuffed VARCHAR — under DYNAMIC, long values overflow cleanly, and the JSON indexing notes show how to keep the blob queryable without dragging it into every row read. If you genuinely need wider on-page rows, innodb_page_size=32K or 64K raises the half-page ceiling (to roughly 16K or 32K), but page size is fixed at instance initialization, so that is a migration project, not an ALTER — and bigger pages carry their own buffer-pool efficiency costs. And when the fix requires a rebuild on a live table, which ALTER TABLE ... ROW_FORMAT always is, run it through your online schema change path rather than raw ALTER; the gh-ost versus pt-osc notes are the honest comparison for that decision.
How do you keep 1118 from ambushing the next schema?
The review habit is arithmetic, and it takes a minute per table. Sum the maximum on-page exposure: fixed columns at their real width, every variable-length column at 768 bytes if the format is COMPACT or 20 bytes if DYNAMIC, and flag anything approaching half a page. Add the declared-width sum for the 65535 limit — utf8mb4 at 4 bytes per character — and flag tables over about 50KB as future migration failures. Both checks are scriptable against information_schema.COLUMNS and worth wiring into schema-change CI, because the entire class of bug is cheaper to catch in a pull request than in a 2 AM error-rate alert. In staging, seed the fattest realistic rows you can construct — maximum payload lengths, every nullable column populated — because a table tested with polite data proves nothing about its physics. And after any row-format migration, verify with the TABLES query above that the format actually changed; an ALTER that gets rewritten or fails over to a replica mid-cutover can leave the old format in place while everyone believes it is fixed. The 8126 error is one of those MySQL limits that looks like a bug until you read it as a guarantee: two rows per page, so the tree stays a tree. Design rows that respect the guarantee, and you never meet the error again.
Where MonPG stands on MySQL
I build MonPG, so the honest line: MonPG monitors PostgreSQL today, and MySQL support is in active development, not shipped. The signals in this piece — 1118 error rates attributed to the specific rows and columns causing them, row-format drift across a fleet, temp-table spills surfacing the same limit mid-query — are exactly what the MySQL work is designed to surface, so a latent width problem shows up as a finding before it shows up as failed INSERTs. The MySQL monitoring (coming soon) page tracks that work as it lands. Until it ships, the same evidence-first approach runs on the PostgreSQL side today, and the rest of these MySQL field notes live on the blog.